From 8cfe75abef49c6c3a40629ddd542a4b9fc5a97a0 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Sat, 8 Jun 2019 18:34:54 +0300 Subject: [PATCH] add admin API for active calls --- .../api/rest/admin/active_calls_controller.rb | 4 + .../api/rest/admin/active_call_resource.rb | 106 ++++++++ config/routes.rb | 1 + spec/factories/active_calls.rb | 111 ++++++++ .../api/rest/admin/active_calls_spec.rb | 247 ++++++++++++++++++ .../returns_json_api_record_relationship.rb | 3 +- 6 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/rest/admin/active_calls_controller.rb create mode 100644 app/resources/api/rest/admin/active_call_resource.rb create mode 100644 spec/factories/active_calls.rb create mode 100644 spec/requests/api/rest/admin/active_calls_spec.rb diff --git a/app/controllers/api/rest/admin/active_calls_controller.rb b/app/controllers/api/rest/admin/active_calls_controller.rb new file mode 100644 index 000000000..8aad110a8 --- /dev/null +++ b/app/controllers/api/rest/admin/active_calls_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Api::Rest::Admin::ActiveCallsController < Api::Rest::Admin::BaseController +end diff --git a/app/resources/api/rest/admin/active_call_resource.rb b/app/resources/api/rest/admin/active_call_resource.rb new file mode 100644 index 000000000..17046b972 --- /dev/null +++ b/app/resources/api/rest/admin/active_call_resource.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class Api::Rest::Admin::ActiveCallResource < ::BaseResource + model_name 'RealtimeData::ActiveCall' + paginator :none + key_type :string + + attributes :start_time, + :connect_time, + :duration, + :time_limit, + :dst_prefix_in, + :dst_prefix_routing, + :lrn, + :dst_prefix_out, + :src_prefix_in, + :src_prefix_routing, + :src_prefix_out, + :diversion_in, + :diversion_out, + :dst_country_id, + :dst_network_id, + :customer_id, + :vendor_id, + :customer_acc_id, + :vendor_acc_id, + :customer_auth_id, + :destination_id, + :dialpeer_id, + :orig_gw_id, + :term_gw_id, + :routing_group_id, + :rateplan_id, + :destination_initial_rate, + :destination_next_rate, + :destination_initial_interval, + :destination_next_interval, + :destination_fee, + :destination_rate_policy_id, + :dialpeer_initial_rate, + :dialpeer_next_rate, + :dialpeer_initial_interval, + :dialpeer_next_interval, + :dialpeer_fee, + :legA_remote_ip, + :legA_remote_port, + :orig_call_id, + :legA_local_ip, + :legA_local_port, + :local_tag, + :legB_local_ip, + :legB_local_port, + :term_call_id, + :legB_remote_ip, + :legB_remote_port, + :node_id, + :pop_id + + has_one :customer, class_name: 'Contractor' + has_one :vendor, class_name: 'Contractor' + has_one :customer_acc, class_name: 'Account' + has_one :vendor_acc, class_name: 'Account' + has_one :customer_auth, class_name: 'CustomersAuth' + has_one :destination, class_name: 'Destination' + has_one :dialpeer, class_name: 'Dialpeer' + has_one :orig_gw, class_name: 'Gateway' + has_one :term_gw, class_name: 'Gateway' + has_one :routing_group, class_name: 'RoutingGroup' + has_one :rateplan, class_name: 'Rateplan' + has_one :destination_rate_policy, class_name: 'DestinationRatePolicy' + has_one :node, class_name: 'Node', foreign_key: :node_id + + filter :node_id_eq + filter :dst_country_id_eq + filter :dst_network_id_eq + filter :vendor_id_eq + filter :customer_id_eq + filter :vendor_acc_id_eq + filter :customer_acc_id_eq + filter :orig_gw_id_eq + filter :term_gw_id_eq + filter :duration_equals + filter :duration_greater_than + filter :duration_less_than + + def self.sortable_fields(_context = nil) + [] + end + + def self.find_by_key(key, options = {}) + context = options[:context] + opts = options.except(:paginator, :sort_criteria) + model = apply_includes(records(opts), opts).find(key) + raise JSONAPI::Exceptions::RecordNotFound, key if model.nil? + + new(model, context) + end + + def self.sort_records(records, _order_options, _context = {}) + records + end + + def self.find_count(_verified_filters, _options) + 0 + end +end diff --git a/config/routes.rb b/config/routes.rb index bfdd1edfc..23260c3d2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -82,6 +82,7 @@ jsonapi_resources :sdp_c_locations jsonapi_resources :session_refresh_methods jsonapi_resources :sortings + jsonapi_resources :active_calls, only: %i[index show destroy] namespace :cdr do jsonapi_resources :cdrs, only: %i[index show] do diff --git a/spec/factories/active_calls.rb b/spec/factories/active_calls.rb new file mode 100644 index 000000000..b1901a7d7 --- /dev/null +++ b/spec/factories/active_calls.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +FactoryGirl.define do + factory :active_call, class: RealtimeData::ActiveCall do + trait :filled do + duration { (rand(60) + rand).round(7) } + start_time { rand(120..179).seconds.ago.to_f } + connect_time { 2.minute.ago.to_f } + end_time nil + local_tag { [1, 2, 16, 8].map { |n| SecureRandom.hex(n).upcase }.join('-') } + local_time { Time.now.to_f } + sequence(:node_id, 1) + vendor_acc_id { Account.vendors_accounts.last&.id || 124 } + vendor_id { Contractor.vendors.last&.id || 123 } + + active_resources '[]' + active_resources_json [] + attempt_num 1 + audio_record_enabled false + auth_orig_ip '192.168.88.23' + auth_orig_port 5060 + auth_orig_protocol_id 1 + cdr_born_time { 1.minute.ago.to_f } + customer_acc_check_balance true + customer_acc_external_id nil + customer_acc_id 25 + customer_acc_vat '0' + customer_auth_external_id nil + customer_auth_id 20_085 + customer_auth_name 'test auth' + customer_external_id nil + customer_id 5 + destination_fee '0.0' + destination_id 4_201_541 + destination_initial_interval 1 + destination_initial_rate '0.11' + destination_next_interval 1 + destination_next_rate '0.11' + destination_prefix '380' + destination_rate_policy_id 1 + destination_reverse_billing false + dialpeer_fee '0.0' + dialpeer_id 1_376_786 + dialpeer_initial_interval 1 + dialpeer_initial_rate '0.005' + dialpeer_next_interval 1 + dialpeer_next_rate '0.001' + dialpeer_prefix '380' + dialpeer_reverse_billing false + disconnect_code 0 + disconnect_initiator 4 + disconnect_internal_code 0 + disconnect_internal_reason 'Unhandled sequence' + disconnect_reason '' + diversion_in nil + diversion_out nil + dst_area_id nil + dst_country_id 222 + dst_network_id 1522 + dst_prefix_in '9810441492550028' + dst_prefix_out '3800000000000000000' + dst_prefix_routing '3800000000000000000' + dump_level_id 0 + from_domain '192.168.88.23' + global_tag '' + legA_local_ip '192.168.88.23' + legA_local_port 5061 + legA_remote_ip '192.168.88.23' + legA_remote_port 5060 + legB_local_ip '' + legB_local_port 0 + legB_remote_ip '' + legB_remote_port 0 + lnp_database_id nil + lrn nil + orig_call_id '2141402782-1223087865-286388420' + orig_gw_external_id nil + orig_gw_id 19 + pai_in nil + pai_out nil + pop_id 4 + ppi_in nil + ppi_out nil + privacy_in nil + privacy_out nil + rateplan_id 18 + resources '' + routing_group_id 24 + routing_plan_id 3 + routing_tag_ids '{}' + rpid_in nil + rpid_out nil + rpid_privacy_in nil + rpid_privacy_out nil + ruri_domain '192.168.88.23' + src_area_id nil + src_name_in '' + src_name_out '' + src_prefix_in '10317' + src_prefix_out '10317' + src_prefix_routing '10317' + term_call_id '8-5D73A55A-5CFB77360000494D-19C01700' + term_gw_external_id nil + term_gw_id 20 + time_limit 4909 + to_domain '192.168.12.88' + vendor_acc_external_id nil + vendor_external_id nil + end + end +end diff --git a/spec/requests/api/rest/admin/active_calls_spec.rb b/spec/requests/api/rest/admin/active_calls_spec.rb new file mode 100644 index 000000000..626aa1dd4 --- /dev/null +++ b/spec/requests/api/rest/admin/active_calls_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Api::Rest::Admin::ContactsController, type: :request do + include_context :json_api_admin_helpers, type: :'active-calls' + let!(:node) { FactoryGirl.create(:node) } + + describe 'GET /api/rest/admin/active-calls' do + subject do + get json_api_request_path, params: json_api_request_params, headers: json_api_request_headers + end + + let(:json_api_request_params) { nil } + + before do + cdrs_filter_stub = instance_double(Yeti::CdrsFilter) + expect(Yeti::CdrsFilter).to receive(:new).with(Node.all, {}).and_return(cdrs_filter_stub) + expect(cdrs_filter_stub).to receive(:search).with(only: nil, empty_on_error: true) + .and_return(active_calls.map(&:stringify_keys)) + end + + context 'with 2 calls' do + let(:active_calls) do + [ + FactoryGirl.attributes_for(:active_call, :filled, node_id: node.id), + FactoryGirl.attributes_for(:active_call, :filled, node_id: node.id) + ] + end + let(:active_calls_ids) { active_calls.map { |r| "#{node.id}*#{r[:local_tag]}" } } + + include_examples :responds_with_status, 200 + include_examples :returns_json_api_collection do + let(:json_api_collection_ids) { active_calls_ids } + end + + context 'include node' do + let(:json_api_request_params) { { include: 'node' } } + + include_examples :responds_with_status, 200 + include_examples :returns_json_api_collection do + let(:json_api_collection_ids) { active_calls_ids } + end + + context 'check node' do + include_examples :returns_json_api_record_relationship, :node do + let(:json_api_record_data) { response_json[:data].first } + let(:json_api_relationship_data) { { id: node.id.to_s, type: 'nodes' } } + end + include_examples :returns_json_api_record_include, type: :nodes do + let(:json_api_include_id) { node.id.to_s } + let(:json_api_include_attributes) { hash_including(name: node.name) } + let(:json_api_include_relationships_names) { nil } + end + end + end + + context 'with include node,customer.smtp-connection' do + let(:json_api_request_params) { { include: 'node,customer.smtp-connection' } } + let(:active_calls) do + result = super() + result.first[:customer_id] = customer.id + result + end + + let!(:smtp_connection) { FactoryGirl.create(:smtp_connection) } + let!(:customer) { FactoryGirl.create(:customer, smtp_connection: smtp_connection) } + + include_examples :responds_with_status, 200 + include_examples :returns_json_api_collection do + let(:json_api_collection_ids) { active_calls_ids } + end + + context 'check node' do + include_examples :returns_json_api_record_relationship, :node do + let(:json_api_record_data) { response_json[:data].first } + let(:json_api_relationship_data) { { id: node.id.to_s, type: 'nodes' } } + end + include_examples :returns_json_api_record_include, type: :nodes do + let(:json_api_include_id) { node.id.to_s } + let(:json_api_include_attributes) { hash_including(name: node.name) } + let(:json_api_include_relationships_names) { nil } + end + end + + context 'check customer' do + include_examples :returns_json_api_record_relationship, :customer do + let(:json_api_record_data) { response_json[:data].first } + let(:json_api_relationship_data) { { id: customer.id.to_s, type: 'contractors' } } + end + include_examples :returns_json_api_record_include, type: :contractors do + let(:json_api_include_id) { customer.id.to_s } + let(:json_api_include_attributes) { hash_including(name: customer.name) } + let(:json_api_include_relationships_names) { [:'smtp-connection'] } + end + end + + context 'check smtp_connection' do + include_examples :returns_json_api_record_relationship, :'smtp-connection' do + let(:json_api_record_data) { response_json[:included].detect { |r| r[:type] == 'contractors' } } + let(:json_api_relationship_data) { { id: smtp_connection.id.to_s, type: 'smtp-connections' } } + end + include_examples :returns_json_api_record_include, type: :'smtp-connections' do + let(:json_api_include_id) { smtp_connection.id.to_s } + let(:json_api_include_attributes) { hash_including(name: smtp_connection.name) } + let(:json_api_include_relationships_names) { nil } + end + end + end + end + + context 'without active calls' do + let(:active_calls) { [] } + + include_examples :responds_with_status, 200 + it 'responds with empty collection' do + subject + expect(response_json[:data]).to eq [] + end + end + end + + describe 'GET /api/rest/admin/active-calls/{id}' do + subject do + get json_api_request_path, params: json_api_request_params, headers: json_api_request_headers + end + + let(:json_api_request_path) { "#{super()}/#{record_id}" } + let(:json_api_request_params) { nil } + let(:record_id) { "#{node.id}*#{local_tag}" } + let(:local_tag) { active_call[:local_tag] } + + before do + expect_any_instance_of(YetisNode::Client).to receive(:calls).with(local_tag).once.and_return(active_call) + end + + let(:active_call) { FactoryGirl.attributes_for(:active_call, :filled, node_id: node.id) } + + rels = %i[ + customer vendor customer-acc vendor-acc customer-auth destination dialpeer + orig-gw term-gw routing-group rateplan destination-rate-policy node + ] + + context 'without includes' do + include_examples :responds_with_status, 200 + include_examples :returns_json_api_record, relationships: rels do + let(:json_api_record_id) { record_id } + let(:json_api_record_attributes) do + hash_including( + 'local-tag': local_tag, + duration: active_call[:duration].to_i, + 'start-time': Time.at(active_call[:start_time].to_i).in_time_zone.as_json, + 'connect-time': Time.at(active_call[:connect_time].to_i).in_time_zone.as_json + ) + end + end + end + + context 'with include node' do + let(:json_api_request_params) { { include: 'node' } } + + include_examples :responds_with_status, 200 + include_examples :returns_json_api_record, relationships: rels do + let(:json_api_record_id) { record_id } + let(:json_api_record_attributes) { hash_including('local-tag': local_tag) } + end + + context 'check node' do + include_examples :returns_json_api_record_relationship, :node do + let(:json_api_relationship_data) { { id: node.id.to_s, type: 'nodes' } } + end + include_examples :returns_json_api_record_include, type: :nodes do + let(:json_api_include_id) { node.id.to_s } + let(:json_api_include_attributes) { hash_including(name: node.name) } + let(:json_api_include_relationships_names) { nil } + end + end + end + + context 'with include node,customer.smtp-connection' do + let(:json_api_request_params) { { include: 'node,customer.smtp-connection' } } + let(:active_call) { super().merge(customer_id: customer.id) } + + let!(:smtp_connection) { FactoryGirl.create(:smtp_connection) } + let!(:customer) { FactoryGirl.create(:customer, smtp_connection: smtp_connection) } + + include_examples :responds_with_status, 200 + include_examples :returns_json_api_record, relationships: rels do + let(:json_api_record_id) { record_id } + let(:json_api_record_attributes) { hash_including('local-tag': local_tag) } + end + + context 'check node' do + include_examples :returns_json_api_record_relationship, :node do + let(:json_api_relationship_data) { { id: node.id.to_s, type: 'nodes' } } + end + include_examples :returns_json_api_record_include, type: :nodes do + let(:json_api_include_id) { node.id.to_s } + let(:json_api_include_attributes) { hash_including(name: node.name) } + let(:json_api_include_relationships_names) { nil } + end + end + + context 'check customer' do + include_examples :returns_json_api_record_relationship, :customer do + let(:json_api_relationship_data) { { id: customer.id.to_s, type: 'contractors' } } + end + include_examples :returns_json_api_record_include, type: :contractors do + let(:json_api_include_id) { customer.id.to_s } + let(:json_api_include_attributes) { hash_including(name: customer.name) } + let(:json_api_include_relationships_names) { [:'smtp-connection'] } + end + end + + context 'check smtp_connection' do + include_examples :returns_json_api_record_relationship, :'smtp-connection' do + let(:json_api_record_data) { response_json[:included].detect { |r| r[:type] == 'contractors' } } + let(:json_api_relationship_data) { { id: smtp_connection.id.to_s, type: 'smtp-connections' } } + end + include_examples :returns_json_api_record_include, type: :'smtp-connections' do + let(:json_api_include_id) { smtp_connection.id.to_s } + let(:json_api_include_attributes) { hash_including(name: smtp_connection.name) } + let(:json_api_include_relationships_names) { nil } + end + end + end + end + + describe 'DELETE /api/rest/admin/active-calls/{id}' do + subject do + delete json_api_request_path, headers: json_api_request_headers + end + + let(:json_api_request_path) { "#{super()}/#{record_id}" } + let(:json_api_request_params) { nil } + let(:record_id) { "#{node.id}*#{local_tag}" } + let(:local_tag) { active_call[:local_tag] } + let(:active_call) { FactoryGirl.attributes_for(:active_call, :filled, node_id: node.id) } + + before do + expect_any_instance_of(YetisNode::Client).to receive(:calls).with(local_tag).once.and_return(active_call) + expect_any_instance_of(YetisNode::Client).to receive(:call_disconnect).with(local_tag).once + end + + include_examples :responds_with_status, 204 + end +end diff --git a/spec/support/examples/json_api/returns_json_api_record_relationship.rb b/spec/support/examples/json_api/returns_json_api_record_relationship.rb index d016165e1..a6bd9b26c 100644 --- a/spec/support/examples/json_api/returns_json_api_record_relationship.rb +++ b/spec/support/examples/json_api/returns_json_api_record_relationship.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples :returns_json_api_record_relationship do |name, status: 200| + let(:json_api_record_data) { response_json[:data] } let(:json_api_relationship_data) { nil } it "returns json api record with correct #{name} relationship data" do @@ -10,7 +11,7 @@ "expect response.status to eq #{status}, but got #{response.status}\n#{pretty_response_json}" ) name = name.to_sym - actual_relationships = response_json[:data][:relationships] + actual_relationships = json_api_record_data[:relationships] expect(actual_relationships.key?(name)).to( eq(true), "expect relationships to have key #{name}, but not found in\n#{actual_relationships}"