telephony: автоформат +380, тип линии SIP/АТС и звонок через Asterisk AMI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 14:52:09 +03:00
parent cc62324b69
commit 5d06d4d3ce
42 changed files with 1158 additions and 57 deletions
+148
View File
@@ -0,0 +1,148 @@
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)