Skip to content

Commit

Permalink
Merge pull request #1646 from yeti-switch/1644-Statistics-WITH-FILL-T…
Browse files Browse the repository at this point in the history
…O-value-cannot-be-less-than-FROM

#1644, Validate date filters for origination-active-calls controller
  • Loading branch information
dmitry-sinina authored Dec 15, 2024
2 parents f562523 + 34740fe commit 10fa6f2
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def show
begin
rows = statistic.collection
render json: rows, status: 200
rescue ClickhouseReport::Base::FromDateTimeInFutureError => e
Rails.logger.error { "<#{e.class}>: #{e.message}" }
render json: [], status: 200
rescue ClickhouseReport::Base::ParamError => e
Rails.logger.error { "Bad Request <#{e.class}>: #{e.message}" }
render json: { error: e.message }, status: 400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def show
begin
rows = statistic.collection
render json: rows, status: 200
rescue ClickhouseReport::Base::FromDateTimeInFutureError => e
Rails.logger.error { "<#{e.class}>: #{e.message}" }
render json: [], status: 200
rescue ClickhouseReport::Base::ParamError => e
Rails.logger.error { "Bad Request <#{e.class}>: #{e.message}" }
render json: { error: e.message }, status: 400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def show
begin
rows = statistic.collection
render json: rows, status: 200
rescue ClickhouseReport::Base::FromDateTimeInFutureError => e
Rails.logger.error { "<#{e.class}>: #{e.message}" }
render json: [], status: 200
rescue ClickhouseReport::Base::ParamError => e
Rails.logger.error { "Bad Request <#{e.class}>: #{e.message}" }
render json: { error: e.message }, status: 400
Expand Down
11 changes: 11 additions & 0 deletions app/lib/date_utilities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module DateUtilities
module_function

def safe_datetime_parse(date_string)
DateTime.parse(date_string)
rescue ArgumentError, TypeError
nil
end
end
2 changes: 2 additions & 0 deletions app/models/clickhouse_report/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class InvalidParamValue < ParamError
class InvalidResponseError < Error
end

FromDateTimeInFutureError = Class.new(ParamError)

class_attribute :operations, instance_accessor: false, default: {}
class_attribute :filters, instance_accessor: false, default: {}

Expand Down
17 changes: 15 additions & 2 deletions app/models/clickhouse_report/origination_active_calls.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,25 @@ class OriginationActiveCalls < Base
column: :snapshot_timestamp,
type: 'DateTime',
operation: :gteq,
required: true
required: true,
format_value: lambda { |value, _options|
from_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value from-time' if from_time.nil?
raise FromDateTimeInFutureError, 'from-time cannot be in the future' if from_time > DateTime.now

value
}

filter :'to-time',
column: :snapshot_timestamp,
type: 'DateTime',
operation: :lteq
operation: :lteq,
format_value: lambda { |value, _options|
to_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value to-time' if to_time.nil?

value
}

filter :'src-country-id',
column: :src_country_id,
Expand Down
17 changes: 15 additions & 2 deletions app/models/clickhouse_report/origination_statistic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ class OriginationStatistic < Base
column: :time_start,
type: 'DateTime',
operation: :gteq,
required: true
required: true,
format_value: lambda { |value, _options|
from_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value from-time' if from_time.nil?
raise FromDateTimeInFutureError, 'from-time cannot be in the future' if from_time > DateTime.now

value
}

filter :'to-time',
column: :time_start,
type: 'DateTime',
operation: :lteq
operation: :lteq,
format_value: lambda { |value, _options|
to_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value to-time' if to_time.nil?

value
}

filter :'src-country-id',
column: :src_country_id,
Expand Down
17 changes: 15 additions & 2 deletions app/models/clickhouse_report/origination_statistic_quality.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,25 @@ class OriginationStatisticQuality < Base
column: :time_start,
type: 'DateTime',
operation: :gteq,
required: true
required: true,
format_value: lambda { |value, _options|
from_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value from-time' if from_time.nil?
raise FromDateTimeInFutureError, 'from-time cannot be in the future' if from_time > DateTime.now

value
}

filter :'to-time',
column: :time_start,
type: 'DateTime',
operation: :lteq
operation: :lteq,
format_value: lambda { |value, _options|
to_time = DateUtilities.safe_datetime_parse(value)
raise InvalidParamValue, 'invalid value to-time' if to_time.nil?

value
}

filter :'src-country-id',
column: :src_country_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

RSpec.describe Api::Rest::Customer::V1::OriginationActiveCallsController do
include_context :json_api_customer_v1_helpers, type: :accounts

let(:auth_headers) { { 'Authorization' => json_api_auth_token } }

describe 'GET /api/rest/customer/v1/origination-active-calls' do
subject { get '/api/rest/customer/v1/origination-active-calls', params: query, headers: auth_headers }

shared_examples :responds_400 do |error_message|
it 'responds with correct error data' do
expect(CaptureError).not_to receive(:capture)
subject

expect(response.status).to eq 400
expect(response_json).to eq error: error_message
end
end

shared_examples :responds_success do
it 'responds with correct data' do
expect(CaptureError).not_to receive(:capture)
subject

expect(response.status).to eq 200
expect(response_json).to eq active_calls_response_body
end
end

let!(:account) { create(:account, contractor: customer) }
let(:clickhouse_query) do
<<-SQL.squish
SELECT
toUnixTimestamp(snapshot_timestamp) as t,
toUInt32(count(*)) AS calls
FROM active_calls
WHERE
customer_acc_id = {account_id: UInt32} AND
snapshot_timestamp >= {from_time: DateTime}
GROUP BY t
ORDER BY t
WITH FILL
FROM toUnixTimestamp(toStartOfMinute({from_time: DateTime}))
TO toUnixTimestamp(now())
STEP 60
FORMAT JSONColumnsWithMetadata
SQL
end
let(:clickhouse_params) do
{
param_account_id: account.id,
param_from_time: from_time
}
end
let!(:stub_clickhouse_query) do
stub_request(:post, ClickHouse.config.url)
.with(
basic_auth: [ClickHouse.config.username, ClickHouse.config.password],
query: {
database: ClickHouse.config.database,
**clickhouse_params,
query: clickhouse_query,
send_progress_in_http_headers: 1
}
).to_return(
status: active_calls_response_status,
body: active_calls_response_body.to_json
)
end
let(:active_calls_response_status) { 200 }
let(:active_calls_response_body) { [{ call_id: 'abc123', duration: 123 }] }
let(:from_time) { 1.day.ago.iso8601 }
let(:query) do
{
'account-id': account.uuid,
'from-time': from_time
}
end

it_behaves_like :responds_success

context 'without params' do
let(:query) { nil }
let(:stub_clickhouse_query) { nil }

include_examples :responds_400, 'missing required param(s) account-id, from-time'
end

context 'with from-time in the future', freeze_time: Time.parse('2024-12-01 00:00:00 UTC') do
let(:query) { super().merge 'from-time': '2024-12-25T05:00:00Z', 'to-time': '2024-12-26T05:00:00Z' }

before { Log::ApiLog.add_partitions }

it 'should NOT perform POST request to ClickHouse server and then return empty collection' do
expect(CaptureError).not_to receive(:capture)
subject

expect(stub_clickhouse_query).not_to have_been_requested
expect(response_json).to eq([])
end
end

context 'when the "from-time" is invalid Date' do
let(:query) { super().merge 'from-time': 'invalid' }

include_examples :responds_400, 'invalid value from-time'
end

context 'when the "to-time" is invalid Date' do
let(:query) { super().merge 'to-time': 'invalid' }

include_examples :responds_400, 'invalid value to-time'
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

RSpec.describe Api::Rest::Customer::V1::OriginationStatisticsController do
include_context :json_api_customer_v1_helpers, type: :accounts

let(:auth_headers) { { 'Authorization' => json_api_auth_token } }

describe 'GET /api/rest/customer/v1/origination-statistics' do
subject { get '/api/rest/customer/v1/origination-statistics', params: query, headers: auth_headers }

shared_examples :responds_400 do |error_message|
it 'responds with correct error data' do
expect(CaptureError).not_to receive(:capture)
subject

expect(response.status).to eq 400
expect(response_json).to eq error: error_message
end
end

shared_examples :responds_success do
it 'responds with correct data' do
expect(CaptureError).not_to receive(:capture)
subject

expect(response.status).to eq 200
expect(response_json).to eq clickhouse_response_body
end
end

let!(:account) { create(:account, contractor: customer) }
let(:clickhouse_response_body) { [{ some_key: 'value' }] }
let(:from_time) { 1.day.ago.iso8601 }
let!(:stub_clickhouse_query) do
stub_request(:post, /#{ClickHouse.config.url}/).and_return(status: 200, body: clickhouse_response_body)
end
let(:query) do
{
'account-id': account.uuid,
'from-time': from_time,
sampling: 'minute'
}
end

context 'when valid params' do
it_behaves_like :responds_success
end

context 'without params' do
let(:query) { {} }
let(:stub_clickhouse_query) { nil }

include_examples :responds_400, 'missing required param(s) account-id, from-time, sampling'
end

context 'with from-time in the future', freeze_time: Time.parse('2024-12-01 00:00:00 UTC') do
let(:query) { super().merge 'from-time': '2024-12-25T05:00:00Z', 'to-time': '2024-12-26T05:00:00Z' }

before { Log::ApiLog.add_partitions }

it 'should NOT perform POST request to ClickHouse server and then return empty collection' do
expect(CaptureError).not_to receive(:capture)
subject

expect(stub_clickhouse_query).not_to have_been_requested
expect(response_json).to eq([])
end
end

context 'when the "from-time" is invalid Date' do
let(:query) { super().merge 'from-time': 'invalid' }

include_examples :responds_400, 'invalid value from-time'
end

context 'when the "to-time" is invalid Date' do
let(:query) { super().merge 'to-time': 'invalid' }

include_examples :responds_400, 'invalid value to-time'
end
end
end
Loading

0 comments on commit 10fa6f2

Please sign in to comment.