diff --git a/app/controllers/api/rest/customer/v1/origination_active_calls_controller.rb b/app/controllers/api/rest/customer/v1/origination_active_calls_controller.rb index 29f35a2d7..f03452d1f 100644 --- a/app/controllers/api/rest/customer/v1/origination_active_calls_controller.rb +++ b/app/controllers/api/rest/customer/v1/origination_active_calls_controller.rb @@ -16,6 +16,9 @@ def show begin rows = statistic.collection render json: rows, status: 200 + rescue ClickhouseReport::OriginationActiveCalls::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 diff --git a/app/lib/date_utilities.rb b/app/lib/date_utilities.rb new file mode 100644 index 000000000..01265bc18 --- /dev/null +++ b/app/lib/date_utilities.rb @@ -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 diff --git a/app/models/clickhouse_report/origination_active_calls.rb b/app/models/clickhouse_report/origination_active_calls.rb index aad612ad2..6d44f2562 100644 --- a/app/models/clickhouse_report/origination_active_calls.rb +++ b/app/models/clickhouse_report/origination_active_calls.rb @@ -2,6 +2,8 @@ module ClickhouseReport class OriginationActiveCalls < Base + FromDateTimeInFutureError = Class.new(ParamError) + filter 'account-id', column: :customer_acc_id, type: 'UInt32', @@ -18,12 +20,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, diff --git a/spec/requests/api/rest/customer/v1/origination_active_calls_controller_spec.rb b/spec/requests/api/rest/customer/v1/origination_active_calls_controller_spec.rb new file mode 100644 index 000000000..234ba5349 --- /dev/null +++ b/spec/requests/api/rest/customer/v1/origination_active_calls_controller_spec.rb @@ -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