from datetime import date from unittest import mock from django.test import Client, SimpleTestCase, TestCase from apps.asterisk_connector.ami import AmiClient, AmiError, parse_ami_packet from apps.asterisk_connector.services import ( TelephonyError, channel_for_employee, originate_between, sanitize_exten, ) from apps.employees.models import Employee from core.models import ConnectorConfig from core.test_utils import login_2fa def make_employee(**overrides): n = Employee.objects.count() + 1 defaults = { 'username': f'ami.user{n}', 'email': f'ami.user{n}@company.local', 'first_name': 'Тест', 'last_name': 'Ами', 'hire_date': date(2026, 1, 1), 'user_location_type': 'pharmacy', 'mattermost_sync_enabled': False, 'ad_sync_enabled': False, 'is_staff': False, } defaults.update(overrides) return Employee.objects.create(**defaults) class AmiProtocolTests(SimpleTestCase): def test_parse_packet(self): packet = parse_ami_packet('Response: Success\r\nMessage: Authentication accepted\r\n') self.assertEqual(packet['Response'], 'Success') def test_send_login_and_originate(self): packets = [ b'Asterisk Call Manager/2.10\r\n', b'Response: Success\r\nMessage: Authentication accepted\r\n\r\n', b'Response: Success\r\nMessage: Originate successfully queued\r\n\r\n', b'Response: Goodbye\r\n\r\n', ] class FakeSock: def __init__(self): self.sent = [] self._packets = list(packets) def recv(self, _n): return self._packets.pop(0) if self._packets else b'' def sendall(self, data): self.sent.append(data) def close(self): pass sock = FakeSock() with mock.patch('apps.asterisk_connector.ami.socket.create_connection', return_value=sock): with AmiClient('127.0.0.1', 5038, 'acore', 'secret') as client: reply = client.originate( channel='PJSIP/101', exten='202', context='from-internal', caller_id='Test <101>', ) self.assertEqual(reply['Response'], 'Success') self.assertTrue(any(b'Action: Login' in chunk for chunk in sock.sent)) self.assertTrue(any(b'Channel: PJSIP/101' in chunk for chunk in sock.sent)) class ChannelMappingTests(TestCase): def test_sip_uses_pjsip_template(self): emp = make_employee(internal_number='101', internal_line_type='sip') self.assertEqual(channel_for_employee(emp), 'PJSIP/101') def test_analog_uses_dahdi(self): emp = make_employee(internal_number='12', internal_line_type='analog') self.assertEqual(channel_for_employee(emp), 'DAHDI/12') def test_custom_template(self): emp = make_employee(internal_number='55', internal_line_type='sip') self.assertEqual( channel_for_employee(emp, {'sip_channel': 'SIP/{exten}'}), 'SIP/55', ) def test_bad_exten(self): with self.assertRaises(TelephonyError): sanitize_exten('ab') class OriginateViewTests(TestCase): def setUp(self): self.caller = make_employee( internal_number='101', internal_line_type='sip', username='ami.caller', email='ami.caller@company.local', ) self.callee = make_employee( internal_number='202', internal_line_type='analog', username='ami.callee', email='ami.callee@company.local', ) ConnectorConfig.objects.create( connector_type='asterisk', is_enabled=True, host='pbx.local', port=5038, username='acore', password='secret', extra_config={'originate_context': 'from-internal'}, ) self.client = Client() login_2fa(self.client, self.caller) def test_originate_posts_to_ami(self): fake = mock.MagicMock() fake.__enter__.return_value = fake fake.__exit__.return_value = False fake.originate.return_value = {'Response': 'Success'} with mock.patch( 'apps.asterisk_connector.services._client_from_config', return_value=fake, ): response = self.client.post('/telephony/call/', {'employee_id': str(self.callee.pk)}) self.assertEqual(response.status_code, 200) self.assertIn('HX-Retarget', response) fake.originate.assert_called_once() kwargs = fake.originate.call_args.kwargs self.assertEqual(kwargs['channel'], 'PJSIP/101') self.assertEqual(kwargs['exten'], '202') def test_disabled_without_internal(self): bare = make_employee(username='ami.bare', email='ami.bare@company.local') login_2fa(self.client, bare) response = self.client.post('/telephony/call/', {'employee_id': str(self.callee.pk)}) self.assertEqual(response.status_code, 400) def test_login_failure_surfaces_message(self): with mock.patch( 'apps.asterisk_connector.services._client_from_config', side_effect=AmiError('Authentication failed'), ): with self.assertRaises(TelephonyError): originate_between(self.caller, self.callee)