diff --git a/lib/dialpad.rb b/lib/dialpad.rb index b234fc4..a2caeee 100644 --- a/lib/dialpad.rb +++ b/lib/dialpad.rb @@ -12,6 +12,7 @@ class APIError < Error; end require 'dialpad/webhook' require 'dialpad/call' +require 'dialpad/call_center' require 'dialpad/contact' require 'dialpad/department' require 'dialpad/subscriptions/call_event' diff --git a/lib/dialpad/call.rb b/lib/dialpad/call.rb index b3049cb..765cfd6 100644 --- a/lib/dialpad/call.rb +++ b/lib/dialpad/call.rb @@ -59,9 +59,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/calllist def list(params = {}) response = Dialpad.client.get('call', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/callactionshangup diff --git a/lib/dialpad/call_center.rb b/lib/dialpad/call_center.rb new file mode 100644 index 0000000..02e61f5 --- /dev/null +++ b/lib/dialpad/call_center.rb @@ -0,0 +1,73 @@ +module Dialpad + class CallCenter < DialpadObject + class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; end + + ATTRIBUTES = %i( + advanced_settings + alerts + availability_status + country + first_action + friday_hours + group_description + hold_queue + hours_on + id + monday_hours + name + no_operators_action + office_id + phone_numbers + ring_seconds + routing_options + state + thursday_hours + timezone + tuesday_hours + voice_intelligence + wednesday_hours + ).freeze + + class << self + include Validations + + # https://developers.dialpad.com/reference/callcentersget + def retrieve(id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.get("callcenters/#{id}") + new(response.body) + end + + # https://developers.dialpad.com/reference/callcenterslistall + def list(params = {}) + response = Dialpad.client.get('callcenters', params) + paginated_response_from(response) + end + + # https://developers.dialpad.com/reference/callcenterscreate + def create(attributes = {}) + validate_required_attributes(attributes, %i(name office_id)) + + response = Dialpad.client.post('callcenters', attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/callcentersupdate + def update(id = nil, attributes = {}) + validate_required_attribute(id, "ID") + + response = Dialpad.client.patch("callcenters/#{id}", attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/callcentersdelete + def destroy(id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.delete("callcenters/#{id}") + new(response.body) + end + end + end +end diff --git a/lib/dialpad/contact.rb b/lib/dialpad/contact.rb index 06a9ed8..f25a010 100644 --- a/lib/dialpad/contact.rb +++ b/lib/dialpad/contact.rb @@ -38,9 +38,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/contactslist def list(params = {}) response = Dialpad.client.get('contacts', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/contactscreate diff --git a/lib/dialpad/department.rb b/lib/dialpad/department.rb index 68d22b2..7e094af 100644 --- a/lib/dialpad/department.rb +++ b/lib/dialpad/department.rb @@ -50,9 +50,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/departmentslistall def list(params = {}) response = Dialpad.client.get('departments', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/departmentscreate diff --git a/lib/dialpad/dialpad_object.rb b/lib/dialpad/dialpad_object.rb index b8f4308..e84a52d 100644 --- a/lib/dialpad/dialpad_object.rb +++ b/lib/dialpad/dialpad_object.rb @@ -1,9 +1,25 @@ module Dialpad + PaginatedResponse = Struct.new(:cursor, :items) + class DialpadObject class RequiredAttributeError < Dialpad::APIError; end attr_reader :attributes + class << self + def paginated_response_from(response) + cursor = response.body['cursor'] + items = + if response.body['items'].nil? + [] + else + response.body['items'].map { |item| new(item) } + end + + PaginatedResponse.new(cursor, items) + end + end + def initialize(attributes = {}) @attributes = attributes.each_with_object({}) do |(key, value), hash| diff --git a/lib/dialpad/subscriptions/call_event.rb b/lib/dialpad/subscriptions/call_event.rb index 2bf314a..4a3fcf1 100644 --- a/lib/dialpad/subscriptions/call_event.rb +++ b/lib/dialpad/subscriptions/call_event.rb @@ -35,9 +35,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/webhook_call_event_subscriptionlist def list(params = {}) response = Dialpad.client.get('subscriptions/call', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/webhook_call_event_subscriptioncreate diff --git a/lib/dialpad/subscriptions/contact_event.rb b/lib/dialpad/subscriptions/contact_event.rb index 885ee19..b64d780 100644 --- a/lib/dialpad/subscriptions/contact_event.rb +++ b/lib/dialpad/subscriptions/contact_event.rb @@ -25,9 +25,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/webhook_contact_event_subscriptionlist def list(params = {}) response = Dialpad.client.get('subscriptions/contact', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/webhook_contact_event_subscriptioncreate diff --git a/lib/dialpad/user.rb b/lib/dialpad/user.rb index cde8b73..2c9a9cf 100644 --- a/lib/dialpad/user.rb +++ b/lib/dialpad/user.rb @@ -13,6 +13,7 @@ class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; e do_not_disturb emails first_name + group_details id image_url international_dialing_enabled @@ -39,12 +40,7 @@ class << self # https://developers.dialpad.com/reference/userslist def list(params = {}) response = Dialpad.client.get('users', params) - return [] if response.body['items'].nil? - - structure = {} - structure[:cursor] = response.body['cursor'] unless response.body['cursor'].nil? - structure[:items] = response.body['items'].map { |item| new(item) } - structure + paginated_response_from(response) end # https://developers.dialpad.com/reference/usersget diff --git a/lib/dialpad/webhook.rb b/lib/dialpad/webhook.rb index ccff574..d2948c1 100644 --- a/lib/dialpad/webhook.rb +++ b/lib/dialpad/webhook.rb @@ -22,9 +22,7 @@ def retrieve(id = nil) # https://developers.dialpad.com/reference/webhookslist def list(params = {}) response = Dialpad.client.get('webhooks', params) - return [] if response.body['items'].nil? - - response.body['items'].map { |item| new(item) } + paginated_response_from(response) end # https://developers.dialpad.com/reference/webhookscreate diff --git a/spec/dialpad/call_center_spec.rb b/spec/dialpad/call_center_spec.rb new file mode 100644 index 0000000..87832ee --- /dev/null +++ b/spec/dialpad/call_center_spec.rb @@ -0,0 +1,696 @@ +require 'spec_helper' + +RSpec.describe Dialpad::CallCenter do + let(:base_url) { 'https://api.dialpad.com' } + let(:token) { 'test_token' } + let(:client) { Dialpad::Client.new(base_url: base_url, token: token) } + + before do + allow(Dialpad).to receive(:client).and_return(client) + end + + describe 'class methods' do + describe '.retrieve' do + context 'with valid ID' do + let(:call_center_data) do + { + 'advanced_settings' => { + 'auto_call_recording' => { + 'call_recording_inbound' => true, + 'call_recording_outbound' => true + }, + 'max_wrap_up_seconds' => '0' + }, + 'alerts' => { + 'cc_service_level' => '95', + 'cc_service_level_seconds' => '60' + }, + 'availability_status' => 'open', + 'country' => 'us', + 'first_action' => 'menu', + 'friday_hours' => ['08:00', '18:00'], + 'group_description' => 'Customer Service Call Center', + 'hold_queue' => { + 'announce_estimated_wait_time' => true, + 'announce_position' => true, + 'announcement_interval_seconds' => '120', + 'estimated_wait_time_max' => '30', + 'max_hold_count' => '50', + 'max_hold_seconds' => '900', + 'queue_callback_dtmf' => '9', + 'queue_callback_threshold' => '5', + 'queue_escape_dtmf' => '*' + }, + 'hours_on' => false, + 'id' => '1234567890123456', + 'monday_hours' => ['08:00', '18:00'], + 'name' => 'Main Call Center', + 'no_operators_action' => 'bridge_target', + 'office_id' => '9876543210987654', + 'phone_numbers' => ['+15551234567', '+15559876543'], + 'ring_seconds' => '30', + 'routing_options' => { + 'closed' => { + 'action' => 'voicemail', + 'dtmf' => [ + { + 'input' => '0', + 'options' => { + 'action' => 'disabled' + } + }, + { + 'input' => '1', + 'options' => { + 'action' => 'directory' + } + } + ], + 'operator_routing' => 'longestidle', + 'try_dial_operators' => false + }, + 'open' => { + 'action' => 'bridge_target', + 'action_target_id' => '1111222233334444', + 'action_target_type' => 'user', + 'dtmf' => [ + { + 'input' => '0', + 'options' => { + 'action' => 'operator' + } + }, + { + 'input' => '1', + 'options' => { + 'action' => 'directory' + } + } + ], + 'operator_routing' => 'longestidle', + 'try_dial_operators' => false + } + }, + 'state' => 'active', + 'thursday_hours' => ['08:00', '18:00'], + 'timezone' => 'US/Pacific', + 'tuesday_hours' => ['08:00', '18:00'], + 'voice_intelligence' => { + 'allow_pause' => true, + 'auto_start' => true + }, + 'wednesday_hours' => ['08:00', '18:00'] + } + end + + it 'retrieves a call center by ID' do + stub_request(:get, "#{base_url}/callcenters/123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: call_center_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + call_center = described_class.retrieve('123') + + expect(call_center).to be_a(described_class) + expect(call_center.id).to eq('1234567890123456') + expect(call_center.name).to eq('Main Call Center') + expect(call_center.office_id).to eq('9876543210987654') + expect(call_center.availability_status).to eq('open') + expect(call_center.country).to eq('us') + expect(call_center.first_action).to eq('menu') + expect(call_center.group_description).to eq('Customer Service Call Center') + expect(call_center.hold_queue).to be_a(Hash) + expect(call_center.hold_queue['max_hold_count']).to eq('50') + expect(call_center.hold_queue['max_hold_seconds']).to eq('900') + expect(call_center.hold_queue['announce_estimated_wait_time']).to be true + expect(call_center.hours_on).to be false + expect(call_center.no_operators_action).to eq('bridge_target') + expect(call_center.phone_numbers).to eq(['+15551234567', '+15559876543']) + expect(call_center.ring_seconds).to eq('30') + expect(call_center.state).to eq('active') + expect(call_center.timezone).to eq('US/Pacific') + expect(call_center.voice_intelligence).to be_a(Hash) + expect(call_center.voice_intelligence['allow_pause']).to be true + expect(call_center.voice_intelligence['auto_start']).to be true + expect(call_center.advanced_settings).to be_a(Hash) + expect(call_center.advanced_settings['max_wrap_up_seconds']).to eq('0') + expect(call_center.alerts).to be_a(Hash) + expect(call_center.alerts['cc_service_level']).to eq('95') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.retrieve(nil) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.retrieve('') }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + end + + describe '.list' do + context 'with call centers' do + let(:call_centers_data) do + { + 'cursor' => 'test_cursor_123', + 'items' => [ + { + 'advanced_settings' => { + 'auto_call_recording' => { + 'call_recording_inbound' => true, + 'call_recording_outbound' => true + }, + 'max_wrap_up_seconds' => '0' + }, + 'alerts' => { + 'cc_service_level' => '95', + 'cc_service_level_seconds' => '60' + }, + 'availability_status' => 'open', + 'country' => 'us', + 'first_action' => 'menu', + 'friday_hours' => ['08:00', '18:00'], + 'group_description' => 'Customer Service Call Center', + 'hold_queue' => { + 'max_hold_count' => '50', + 'max_hold_seconds' => '900' + }, + 'hours_on' => false, + 'id' => '1234567890123456', + 'monday_hours' => ['08:00', '18:00'], + 'name' => 'Main Call Center', + 'no_operators_action' => 'bridge_target', + 'office_id' => '9876543210987654', + 'phone_numbers' => ['+15551234567'], + 'ring_seconds' => '30', + 'state' => 'active', + 'timezone' => 'US/Pacific', + 'voice_intelligence' => { + 'allow_pause' => true, + 'auto_start' => true + } + }, + { + 'advanced_settings' => { + 'auto_call_recording' => { + 'call_recording_inbound' => false, + 'call_recording_outbound' => false + }, + 'max_wrap_up_seconds' => '30' + }, + 'alerts' => { + 'cc_service_level' => '90', + 'cc_service_level_seconds' => '45' + }, + 'availability_status' => 'closed', + 'country' => 'ca', + 'first_action' => 'operator', + 'friday_hours' => ['09:00', '17:00'], + 'group_description' => 'Support Call Center', + 'hold_queue' => { + 'max_hold_count' => '25', + 'max_hold_seconds' => '600' + }, + 'hours_on' => true, + 'id' => '1234567890123457', + 'monday_hours' => ['09:00', '17:00'], + 'name' => 'Support Call Center', + 'no_operators_action' => 'voicemail', + 'office_id' => '9876543210987654', + 'phone_numbers' => ['+15559876543'], + 'ring_seconds' => '20', + 'state' => 'inactive', + 'timezone' => 'America/Toronto', + 'voice_intelligence' => { + 'allow_pause' => false, + 'auto_start' => false + } + } + ] + } + end + + it 'returns a PaginatedResponse with cursor and items' do + stub_request(:get, "#{base_url}/callcenters") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: call_centers_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to eq('test_cursor_123') + expect(result.items).to be_an(Array) + expect(result.items.length).to eq(2) + expect(result.items.first).to be_a(described_class) + expect(result.items.first.id).to eq('1234567890123456') + expect(result.items.first.name).to eq('Main Call Center') + expect(result.items.first.availability_status).to eq('open') + expect(result.items.last.id).to eq('1234567890123457') + expect(result.items.last.name).to eq('Support Call Center') + expect(result.items.last.availability_status).to eq('closed') + end + + it 'passes query parameters to API' do + params = { 'limit' => 10, 'offset' => 0, 'office_id' => '9876543210987654' } + stub_request(:get, "#{base_url}/callcenters") + .with( + headers: { 'Authorization' => "Bearer #{token}" }, + query: params + ) + .to_return(status: 200, body: call_centers_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + described_class.list(params) + end + + it 'returns PaginatedResponse with nil cursor when cursor is nil' do + call_centers_data_no_cursor = call_centers_data.dup + call_centers_data_no_cursor.delete('cursor') + + stub_request(:get, "#{base_url}/callcenters") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: call_centers_data_no_cursor.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to be_an(Array) + expect(result.items.length).to eq(2) + end + end + + context 'with no call centers' do + it 'returns PaginatedResponse with empty items when items is blank' do + stub_request(:get, "#{base_url}/callcenters") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) + end + + it 'returns PaginatedResponse with empty items when items is nil' do + stub_request(:get, "#{base_url}/callcenters") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) + end + end + end + + describe '.create' do + context 'with valid attributes' do + let(:create_attributes) do + { + name: 'New Call Center', + office_id: '9876543210987654', + availability_status: 'open', + first_action: 'menu', + ring_seconds: '30' + } + end + + let(:created_call_center_data) do + { + 'id' => '1234567890123458', + 'name' => 'New Call Center', + 'office_id' => '9876543210987654', + 'availability_status' => 'open', + 'first_action' => 'menu', + 'ring_seconds' => '30', + 'country' => 'us', + 'state' => 'active', + 'timezone' => 'US/Pacific', + 'hold_queue' => { + 'max_hold_count' => '50', + 'max_hold_seconds' => '900' + }, + 'voice_intelligence' => { + 'allow_pause' => true, + 'auto_start' => false + }, + 'advanced_settings' => { + 'auto_call_recording' => { + 'call_recording_inbound' => false, + 'call_recording_outbound' => false + }, + 'max_wrap_up_seconds' => '0' + } + } + end + + it 'creates a new call center' do + stub_request(:post, "#{base_url}/callcenters") + .with( + headers: { 'Authorization' => "Bearer #{token}" }, + body: create_attributes.to_json + ) + .to_return(status: 201, body: created_call_center_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + call_center = described_class.create(create_attributes) + + expect(call_center).to be_a(described_class) + expect(call_center.id).to eq('1234567890123458') + expect(call_center.name).to eq('New Call Center') + expect(call_center.office_id).to eq('9876543210987654') + expect(call_center.availability_status).to eq('open') + expect(call_center.first_action).to eq('menu') + expect(call_center.ring_seconds).to eq('30') + end + end + + context 'with missing required attributes' do + it 'raises RequiredAttributeError when name is missing' do + expect { described_class.create({ office_id: 123 }) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attributes: name' + ) + end + + it 'raises RequiredAttributeError when office_id is missing' do + expect { described_class.create({ name: 'Test Call Center' }) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attributes: office_id' + ) + end + + it 'raises RequiredAttributeError when both required attributes are missing' do + expect { described_class.create({}) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attributes: name, office_id' + ) + end + end + end + + describe '.update' do + context 'with valid ID and attributes' do + let(:update_attributes) do + { + name: 'Updated Call Center', + ring_seconds: '45', + availability_status: 'closed' + } + end + + let(:updated_call_center_data) do + { + 'id' => '1234567890123456', + 'name' => 'Updated Call Center', + 'office_id' => '9876543210987654', + 'availability_status' => 'closed', + 'ring_seconds' => '45', + 'country' => 'us', + 'state' => 'active', + 'timezone' => 'US/Pacific', + 'hold_queue' => { + 'max_hold_count' => '50', + 'max_hold_seconds' => '900' + }, + 'voice_intelligence' => { + 'allow_pause' => true, + 'auto_start' => true + }, + 'advanced_settings' => { + 'auto_call_recording' => { + 'call_recording_inbound' => true, + 'call_recording_outbound' => true + }, + 'max_wrap_up_seconds' => '0' + } + } + end + + it 'updates a call center' do + stub_request(:patch, "#{base_url}/callcenters/123") + .with( + headers: { 'Authorization' => "Bearer #{token}" }, + body: update_attributes.to_json + ) + .to_return(status: 200, body: updated_call_center_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + call_center = described_class.update('123', update_attributes) + + expect(call_center).to be_a(described_class) + expect(call_center.id).to eq('1234567890123456') + expect(call_center.name).to eq('Updated Call Center') + expect(call_center.availability_status).to eq('closed') + expect(call_center.ring_seconds).to eq('45') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.update(nil, {}) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.update('', {}) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + end + + describe '.destroy' do + context 'with valid ID' do + let(:destroyed_call_center_data) do + { + 'id' => '1234567890123456', + 'name' => 'Main Call Center', + 'office_id' => '9876543210987654', + 'deleted' => true + } + end + + it 'destroys a call center' do + stub_request(:delete, "#{base_url}/callcenters/123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: destroyed_call_center_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + call_center = described_class.destroy('123') + + expect(call_center).to be_a(described_class) + expect(call_center.id).to eq('1234567890123456') + expect(call_center.name).to eq('Main Call Center') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.destroy(nil) }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.destroy('') }.to raise_error( + Dialpad::CallCenter::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + end + end + + describe 'instance methods' do + let(:call_center_attributes) do + { + advanced_settings: { + 'auto_call_recording' => { + 'call_recording_inbound' => true, + 'call_recording_outbound' => true + }, + 'max_wrap_up_seconds' => '0' + }, + alerts: { + 'cc_service_level' => '95', + 'cc_service_level_seconds' => '60' + }, + availability_status: 'open', + country: 'us', + first_action: 'menu', + friday_hours: ['08:00', '18:00'], + group_description: 'Customer Service Call Center', + hold_queue: { + 'announce_estimated_wait_time' => true, + 'announce_position' => true, + 'announcement_interval_seconds' => '120', + 'max_hold_count' => '50', + 'max_hold_seconds' => '900' + }, + hours_on: false, + id: '1234567890123456', + monday_hours: ['08:00', '18:00'], + name: 'Main Call Center', + no_operators_action: 'bridge_target', + office_id: '9876543210987654', + phone_numbers: ['+15551234567', '+15559876543'], + ring_seconds: '30', + routing_options: { + 'closed' => { + 'action' => 'voicemail', + 'operator_routing' => 'longestidle', + 'try_dial_operators' => false + }, + 'open' => { + 'action' => 'bridge_target', + 'action_target_id' => '1111222233334444', + 'action_target_type' => 'user', + 'operator_routing' => 'longestidle', + 'try_dial_operators' => false + } + }, + state: 'active', + thursday_hours: ['08:00', '18:00'], + timezone: 'US/Pacific', + tuesday_hours: ['08:00', '18:00'], + voice_intelligence: { + 'allow_pause' => true, + 'auto_start' => true + }, + wednesday_hours: ['08:00', '18:00'] + } + end + + let(:call_center) { described_class.new(call_center_attributes) } + + describe '#initialize' do + it 'sets attributes from hash' do + expect(call_center.id).to eq('1234567890123456') + expect(call_center.name).to eq('Main Call Center') + expect(call_center.office_id).to eq('9876543210987654') + expect(call_center.availability_status).to eq('open') + expect(call_center.country).to eq('us') + expect(call_center.first_action).to eq('menu') + expect(call_center.friday_hours).to eq(['08:00', '18:00']) + expect(call_center.group_description).to eq('Customer Service Call Center') + expect(call_center.hold_queue).to be_a(Hash) + expect(call_center.hold_queue['max_hold_count']).to eq('50') + expect(call_center.hold_queue['max_hold_seconds']).to eq('900') + expect(call_center.hold_queue['announce_estimated_wait_time']).to be true + expect(call_center.hours_on).to be false + expect(call_center.monday_hours).to eq(['08:00', '18:00']) + expect(call_center.no_operators_action).to eq('bridge_target') + expect(call_center.phone_numbers).to eq(['+15551234567', '+15559876543']) + expect(call_center.ring_seconds).to eq('30') + expect(call_center.routing_options).to be_a(Hash) + expect(call_center.state).to eq('active') + expect(call_center.thursday_hours).to eq(['08:00', '18:00']) + expect(call_center.timezone).to eq('US/Pacific') + expect(call_center.tuesday_hours).to eq(['08:00', '18:00']) + expect(call_center.voice_intelligence).to be_a(Hash) + expect(call_center.voice_intelligence['allow_pause']).to be true + expect(call_center.voice_intelligence['auto_start']).to be true + expect(call_center.wednesday_hours).to eq(['08:00', '18:00']) + expect(call_center.advanced_settings).to be_a(Hash) + expect(call_center.advanced_settings['max_wrap_up_seconds']).to eq('0') + expect(call_center.alerts).to be_a(Hash) + expect(call_center.alerts['cc_service_level']).to eq('95') + end + + it 'converts string keys to symbols' do + call_center_with_string_keys = described_class.new( + 'id' => '1234567890123456', + 'name' => 'Main Call Center', + 'office_id' => '9876543210987654' + ) + + expect(call_center_with_string_keys.id).to eq('1234567890123456') + expect(call_center_with_string_keys.name).to eq('Main Call Center') + expect(call_center_with_string_keys.office_id).to eq('9876543210987654') + end + + it 'handles empty attributes' do + empty_call_center = described_class.new({}) + expect(empty_call_center.attributes).to eq({}) + end + end + + describe 'attribute access' do + it 'allows access to all defined attributes' do + expect(call_center).to respond_to(:advanced_settings) + expect(call_center).to respond_to(:alerts) + expect(call_center).to respond_to(:availability_status) + expect(call_center).to respond_to(:country) + expect(call_center).to respond_to(:first_action) + expect(call_center).to respond_to(:friday_hours) + expect(call_center).to respond_to(:group_description) + expect(call_center).to respond_to(:hold_queue) + expect(call_center).to respond_to(:hours_on) + expect(call_center).to respond_to(:id) + expect(call_center).to respond_to(:monday_hours) + expect(call_center).to respond_to(:name) + expect(call_center).to respond_to(:no_operators_action) + expect(call_center).to respond_to(:office_id) + expect(call_center).to respond_to(:phone_numbers) + expect(call_center).to respond_to(:ring_seconds) + expect(call_center).to respond_to(:routing_options) + expect(call_center).to respond_to(:state) + expect(call_center).to respond_to(:thursday_hours) + expect(call_center).to respond_to(:timezone) + expect(call_center).to respond_to(:tuesday_hours) + expect(call_center).to respond_to(:voice_intelligence) + expect(call_center).to respond_to(:wednesday_hours) + end + + it 'raises NoMethodError for undefined attributes' do + expect { call_center.undefined_attribute }.to raise_error(NoMethodError) + end + end + end + + describe 'error handling' do + it 'defines RequiredAttributeError' do + expect(Dialpad::CallCenter::RequiredAttributeError).to be < Dialpad::DialpadObject::RequiredAttributeError + end + end + + describe 'API integration' do + context 'when API returns error' do + it 'handles 404 errors gracefully' do + stub_request(:get, "#{base_url}/callcenters/nonexistent") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 404, body: 'Not Found') + + expect { described_class.retrieve('nonexistent') }.to raise_error(Dialpad::APIError, /404 - Not Found/) + end + + it 'handles 401 errors gracefully' do + stub_request(:get, "#{base_url}/callcenters/123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 401, body: 'Unauthorized') + + expect { described_class.retrieve('123') }.to raise_error(Dialpad::APIError, /401 - Unauthorized/) + end + + it 'handles 422 errors for create with invalid data' do + stub_request(:post, "#{base_url}/callcenters") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 422, body: 'Unprocessable Entity') + + expect { described_class.create({ name: 'Test', office_id: 123 }) }.to raise_error(Dialpad::APIError, /422 - Unprocessable Entity/) + end + end + end +end diff --git a/spec/dialpad/call_spec.rb b/spec/dialpad/call_spec.rb index d43f29a..4c4f4a6 100644 --- a/spec/dialpad/call_spec.rb +++ b/spec/dialpad/call_spec.rb @@ -161,15 +161,15 @@ calls = described_class.list - expect(calls).to be_an(Array) - expect(calls.length).to eq(2) - expect(calls.first).to be_a(described_class) - expect(calls.first.call_id).to eq(5780678246121472) - expect(calls.first.direction).to eq('outbound') - expect(calls.first.state).to eq('calling') - expect(calls.last.call_id).to eq(5780678246121473) - expect(calls.last.direction).to eq('inbound') - expect(calls.last.state).to eq('completed') + expect(calls).to be_a(Dialpad::PaginatedResponse) + expect(calls.items.length).to eq(2) + expect(calls.items.first).to be_a(described_class) + expect(calls.items.first.call_id).to eq(5780678246121472) + expect(calls.items.first.direction).to eq('outbound') + expect(calls.items.first.state).to eq('calling') + expect(calls.items.last.call_id).to eq(5780678246121473) + expect(calls.items.last.direction).to eq('inbound') + expect(calls.items.last.state).to eq('completed') end it 'passes query parameters to API' do @@ -187,24 +187,26 @@ end context 'with no calls' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/call") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) calls = described_class.list - expect(calls).to eq([]) + expect(calls).to be_a(Dialpad::PaginatedResponse) + expect(calls.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/call") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) calls = described_class.list - expect(calls).to eq([]) + expect(calls).to be_a(Dialpad::PaginatedResponse) + expect(calls.items).to eq([]) end end end diff --git a/spec/dialpad/contact_spec.rb b/spec/dialpad/contact_spec.rb index c0ddc22..64d53ec 100644 --- a/spec/dialpad/contact_spec.rb +++ b/spec/dialpad/contact_spec.rb @@ -73,18 +73,18 @@ } end - it 'returns an array of contacts' do + it 'returns a PaginatedResponse with items' do stub_request(:get, "#{base_url}/contacts") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: contacts_data.to_json, headers: { 'Content-Type' => 'application/json' }) contacts = described_class.list - expect(contacts).to be_an(Array) - expect(contacts.length).to eq(2) - expect(contacts.first).to be_a(described_class) - expect(contacts.first.id).to eq('123') - expect(contacts.first.first_name).to eq('John') + expect(contacts).to be_a(Dialpad::PaginatedResponse) + expect(contacts.items.length).to eq(2) + expect(contacts.items.first).to be_a(described_class) + expect(contacts.items.first.id).to eq('123') + expect(contacts.items.first.first_name).to eq('John') end it 'passes query parameters' do @@ -101,24 +101,26 @@ end context 'with no contacts' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/contacts") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) contacts = described_class.list - expect(contacts).to eq([]) + expect(contacts).to be_a(Dialpad::PaginatedResponse) + expect(contacts.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/contacts") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) contacts = described_class.list - expect(contacts).to eq([]) + expect(contacts).to be_a(Dialpad::PaginatedResponse) + expect(contacts.items).to eq([]) end end end diff --git a/spec/dialpad/department_spec.rb b/spec/dialpad/department_spec.rb index 3550668..546679e 100644 --- a/spec/dialpad/department_spec.rb +++ b/spec/dialpad/department_spec.rb @@ -140,6 +140,7 @@ context 'with departments' do let(:departments_data) do { + 'cursor' => 'dept_cursor_789', 'items' => [ { 'id' => '1234567890123456', @@ -193,22 +194,24 @@ } end - it 'returns an array of departments' do + it 'returns a PaginatedResponse with departments' do stub_request(:get, "#{base_url}/departments") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: departments_data.to_json, headers: { 'Content-Type' => 'application/json' }) - departments = described_class.list - - expect(departments).to be_an(Array) - expect(departments.length).to eq(2) - expect(departments.first).to be_a(described_class) - expect(departments.first.id).to eq('1234567890123456') - expect(departments.first.name).to eq('Sales Department') - expect(departments.first.availability_status).to eq('open') - expect(departments.last.id).to eq('1234567890123457') - expect(departments.last.name).to eq('Support Department') - expect(departments.last.availability_status).to eq('closed') + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to eq('dept_cursor_789') + expect(result.items).to be_an(Array) + expect(result.items.length).to eq(2) + expect(result.items.first).to be_a(described_class) + expect(result.items.first.id).to eq('1234567890123456') + expect(result.items.first.name).to eq('Sales Department') + expect(result.items.first.availability_status).to eq('open') + expect(result.items.last.id).to eq('1234567890123457') + expect(result.items.last.name).to eq('Support Department') + expect(result.items.last.availability_status).to eq('closed') end it 'passes query parameters to API' do @@ -226,24 +229,28 @@ end context 'with no departments' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/departments") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) - departments = described_class.list + result = described_class.list - expect(departments).to eq([]) + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/departments") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) - departments = described_class.list + result = described_class.list - expect(departments).to eq([]) + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) end end end diff --git a/spec/dialpad/subscriptions/call_event_spec.rb b/spec/dialpad/subscriptions/call_event_spec.rb index 967c29a..a3e9b0e 100644 --- a/spec/dialpad/subscriptions/call_event_spec.rb +++ b/spec/dialpad/subscriptions/call_event_spec.rb @@ -103,21 +103,21 @@ } end - it 'returns an array of call event subscriptions' do + it 'returns a PaginatedResponse with items' do stub_request(:get, "#{base_url}/subscriptions/call") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: call_events_data.to_json, headers: { 'Content-Type' => 'application/json' }) call_events = described_class.list - expect(call_events).to be_an(Array) - expect(call_events.length).to eq(2) - expect(call_events.first).to be_a(described_class) - expect(call_events.first.id).to eq('4614441776955392') - expect(call_events.first.call_states).to eq(['calling']) - expect(call_events.last.id).to eq('4614441776955393') - expect(call_events.last.call_states).to eq(['completed', 'failed']) - expect(call_events.last.group_calls_only).to be true + expect(call_events).to be_a(Dialpad::PaginatedResponse) + expect(call_events.items.length).to eq(2) + expect(call_events.items.first).to be_a(described_class) + expect(call_events.items.first.id).to eq('4614441776955392') + expect(call_events.items.first.call_states).to eq(['calling']) + expect(call_events.items.last.id).to eq('4614441776955393') + expect(call_events.items.last.call_states).to eq(['completed', 'failed']) + expect(call_events.items.last.group_calls_only).to be true end it 'passes query parameters to API' do @@ -134,24 +134,26 @@ end context 'with no call event subscriptions' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/subscriptions/call") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) call_events = described_class.list - expect(call_events).to eq([]) + expect(call_events).to be_a(Dialpad::PaginatedResponse) + expect(call_events.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/subscriptions/call") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) call_events = described_class.list - expect(call_events).to eq([]) + expect(call_events).to be_a(Dialpad::PaginatedResponse) + expect(call_events.items).to eq([]) end end end diff --git a/spec/dialpad/subscriptions/contact_event_spec.rb b/spec/dialpad/subscriptions/contact_event_spec.rb index a9a11f6..8864fbd 100644 --- a/spec/dialpad/subscriptions/contact_event_spec.rb +++ b/spec/dialpad/subscriptions/contact_event_spec.rb @@ -99,20 +99,20 @@ } end - it 'returns an array of contact event subscriptions' do + it 'returns a PaginatedResponse with items' do stub_request(:get, "#{base_url}/subscriptions/contact") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: contact_events_data.to_json, headers: { 'Content-Type' => 'application/json' }) contact_events = described_class.list - expect(contact_events).to be_an(Array) - expect(contact_events.length).to eq(2) - expect(contact_events.first).to be_a(described_class) - expect(contact_events.first.id).to eq('5923790016724992') - expect(contact_events.first.contact_type).to eq('shared') - expect(contact_events.last.id).to eq('5923790016724993') - expect(contact_events.last.contact_type).to eq('user') + expect(contact_events).to be_a(Dialpad::PaginatedResponse) + expect(contact_events.items.length).to eq(2) + expect(contact_events.items.first).to be_a(described_class) + expect(contact_events.items.first.id).to eq('5923790016724992') + expect(contact_events.items.first.contact_type).to eq('shared') + expect(contact_events.items.last.id).to eq('5923790016724993') + expect(contact_events.items.last.contact_type).to eq('user') end it 'passes query parameters to API' do @@ -129,24 +129,26 @@ end context 'with no contact event subscriptions' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/subscriptions/contact") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) contact_events = described_class.list - expect(contact_events).to eq([]) + expect(contact_events).to be_a(Dialpad::PaginatedResponse) + expect(contact_events.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/subscriptions/contact") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) contact_events = described_class.list - expect(contact_events).to eq([]) + expect(contact_events).to be_a(Dialpad::PaginatedResponse) + expect(contact_events.items).to eq([]) end end end diff --git a/spec/dialpad/user_spec.rb b/spec/dialpad/user_spec.rb index 9453ffe..f62396c 100644 --- a/spec/dialpad/user_spec.rb +++ b/spec/dialpad/user_spec.rb @@ -45,7 +45,15 @@ 'is_default_voicemail' => true, 'name' => 'default', 'voicemail_notifications_enabled' => true - } + }, + 'group_details' => [ + { + 'do_not_disturb' => false, + 'group_id' => '6178959861465088', + 'group_type' => 'callcenter', + 'role' => 'operator' + } + ] } end @@ -88,6 +96,7 @@ context 'with users' do let(:users_data) do { + 'cursor' => 'user_cursor_456', 'items' => [ { 'id' => '1111222233334444', @@ -109,19 +118,47 @@ } end - it 'returns an array of users' do + it 'returns a PaginatedResponse with users' do stub_request(:get, "#{base_url}/users") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: users_data.to_json, headers: { 'Content-Type' => 'application/json' }) - users = described_class.list[:items] + result = described_class.list - expect(users).to be_an(Array) - expect(users.length).to eq(2) - expect(users.first).to be_a(described_class) - expect(users.first.id).to eq('1111222233334444') - expect(users.first.first_name).to eq('John') - expect(users.first.is_admin).to be true + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to eq('user_cursor_456') + expect(result.items).to be_an(Array) + expect(result.items.length).to eq(2) + expect(result.items.first).to be_a(described_class) + expect(result.items.first.id).to eq('1111222233334444') + expect(result.items.first.first_name).to eq('John') + expect(result.items.first.is_admin).to be true + end + end + + context 'with no users' do + it 'returns PaginatedResponse with empty items when items is blank' do + stub_request(:get, "#{base_url}/users") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) + end + + it 'returns PaginatedResponse with empty items when items is nil' do + stub_request(:get, "#{base_url}/users") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) + + result = described_class.list + + expect(result).to be_a(Dialpad::PaginatedResponse) + expect(result.cursor).to be_nil + expect(result.items).to eq([]) end end end @@ -348,7 +385,15 @@ is_default_voicemail: true, name: 'default', voicemail_notifications_enabled: true - } + }, + group_details: [ + { + do_not_disturb: false, + group_id: '6178959861465088', + group_type: 'callcenter', + role: 'operator' + } + ] } end @@ -408,6 +453,14 @@ expect(user.admin_office_ids).to eq(['1234567890123456']) expect(user.emails).to eq(['john.doe@example.com']) expect(user.phone_numbers).to eq(['+15551234567']) + expect(user.group_details).to eq([ + { + do_not_disturb: false, + group_id: '6178959861465088', + group_type: 'callcenter', + role: 'operator' + } + ]) end end @@ -441,6 +494,7 @@ expect(user).to respond_to(:date_added) expect(user).to respond_to(:date_first_login) expect(user).to respond_to(:voicemail) + expect(user).to respond_to(:group_details) end it 'raises NoMethodError for undefined attributes' do diff --git a/spec/dialpad/webhook_spec.rb b/spec/dialpad/webhook_spec.rb index c254223..665264f 100644 --- a/spec/dialpad/webhook_spec.rb +++ b/spec/dialpad/webhook_spec.rb @@ -85,20 +85,20 @@ } end - it 'returns an array of webhooks' do + it 'returns a PaginatedResponse with items' do stub_request(:get, "#{base_url}/webhooks") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: webhooks_data.to_json, headers: { 'Content-Type' => 'application/json' }) webhooks = described_class.list - expect(webhooks).to be_an(Array) - expect(webhooks.length).to eq(2) - expect(webhooks.first).to be_a(described_class) - expect(webhooks.first.id).to eq('5159136949157888') - expect(webhooks.first.hook_url).to eq('https://example.com/webhooks/dialpad/call') - expect(webhooks.last.id).to eq('5159136949157889') - expect(webhooks.last.hook_url).to eq('https://example.com/webhooks/dialpad/contact') + expect(webhooks).to be_a(Dialpad::PaginatedResponse) + expect(webhooks.items.length).to eq(2) + expect(webhooks.items.first).to be_a(described_class) + expect(webhooks.items.first.id).to eq('5159136949157888') + expect(webhooks.items.first.hook_url).to eq('https://example.com/webhooks/dialpad/call') + expect(webhooks.items.last.id).to eq('5159136949157889') + expect(webhooks.items.last.hook_url).to eq('https://example.com/webhooks/dialpad/contact') end it 'passes query parameters to API' do @@ -115,24 +115,26 @@ end context 'with no webhooks' do - it 'returns empty array when items is blank' do + it 'returns PaginatedResponse with empty items when items is blank' do stub_request(:get, "#{base_url}/webhooks") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: { 'items' => [] }.to_json, headers: { 'Content-Type' => 'application/json' }) webhooks = described_class.list - expect(webhooks).to eq([]) + expect(webhooks).to be_a(Dialpad::PaginatedResponse) + expect(webhooks.items).to eq([]) end - it 'returns empty array when items is nil' do + it 'returns PaginatedResponse with empty items when items is nil' do stub_request(:get, "#{base_url}/webhooks") .with(headers: { 'Authorization' => "Bearer #{token}" }) .to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' }) webhooks = described_class.list - expect(webhooks).to eq([]) + expect(webhooks).to be_a(Dialpad::PaginatedResponse) + expect(webhooks.items).to eq([]) end end end