diff --git a/backend/tests/unit/test_phone_calls.py b/backend/tests/unit/test_phone_calls.py index 9afdc7577d..fa798a6a91 100644 --- a/backend/tests/unit/test_phone_calls.py +++ b/backend/tests/unit/test_phone_calls.py @@ -1,5 +1,6 @@ import os import sys +from types import ModuleType, SimpleNamespace from unittest.mock import MagicMock, patch os.environ.setdefault("ENCRYPTION_SECRET", "omi_ZwB2ZNqB2HHpMK6wStk7sTpavJiPTFg7gXUHnc4tFABPU6pZ2c2DKgehtfgi4RZv") @@ -10,6 +11,80 @@ sys.modules.setdefault("firebase_admin", _mock_firebase) sys.modules.setdefault("firebase_admin.auth", _mock_firebase.auth) + +class _TwilioRestException(Exception): + def __init__(self, status=None, uri=None, msg='', code=None, method=None, details=None): + super().__init__(msg) + self.status = status + self.uri = uri + self.msg = msg + self.code = code + self.method = method + self.details = details + + +class _VoiceResponse: + def __init__(self): + self._children = [] + + def say(self, message): + self._children.append(f'{message}') + + def append(self, child): + self._children.append(str(child)) + + def __str__(self): + return '' + ''.join(self._children) + '' + + +class _Dial: + def __init__(self, caller_id=None, time_limit=None, **_kwargs): + self._caller_id = caller_id + self._time_limit = time_limit + self._numbers = [] + + def number(self, number): + self._numbers.append(number) + + def __str__(self): + attrs = [] + if self._caller_id is not None: + attrs.append(f'callerId="{self._caller_id}"') + if self._time_limit is not None: + attrs.append(f'timeLimit="{self._time_limit}"') + attrs_text = (' ' + ' '.join(attrs)) if attrs else '' + numbers = ''.join(f'{number}' for number in self._numbers) + return f'{numbers}' + + +_twilio = ModuleType('twilio') +_twilio_base = ModuleType('twilio.base') +_twilio_base_exceptions = ModuleType('twilio.base.exceptions') +_twilio_base_exceptions.TwilioRestException = _TwilioRestException +_twilio_twiml = ModuleType('twilio.twiml') +_twilio_twiml_voice_response = ModuleType('twilio.twiml.voice_response') +_twilio_twiml_voice_response.VoiceResponse = _VoiceResponse +_twilio_twiml_voice_response.Dial = _Dial +_twilio_rest = ModuleType('twilio.rest') +_twilio_rest.Client = MagicMock +_twilio_jwt = ModuleType('twilio.jwt') +_twilio_jwt_access_token = ModuleType('twilio.jwt.access_token') +_twilio_jwt_access_token.AccessToken = MagicMock +_twilio_jwt_access_token_grants = ModuleType('twilio.jwt.access_token.grants') +_twilio_jwt_access_token_grants.VoiceGrant = MagicMock +_twilio_request_validator = ModuleType('twilio.request_validator') +_twilio_request_validator.RequestValidator = MagicMock +sys.modules.setdefault('twilio', _twilio) +sys.modules.setdefault('twilio.base', _twilio_base) +sys.modules.setdefault('twilio.base.exceptions', _twilio_base_exceptions) +sys.modules.setdefault('twilio.twiml', _twilio_twiml) +sys.modules.setdefault('twilio.twiml.voice_response', _twilio_twiml_voice_response) +sys.modules.setdefault('twilio.rest', _twilio_rest) +sys.modules.setdefault('twilio.jwt', _twilio_jwt) +sys.modules.setdefault('twilio.jwt.access_token', _twilio_jwt_access_token) +sys.modules.setdefault('twilio.jwt.access_token.grants', _twilio_jwt_access_token_grants) +sys.modules.setdefault('twilio.request_validator', _twilio_request_validator) + import pytest from fastapi import FastAPI from fastapi.testclient import TestClient @@ -24,6 +99,16 @@ TEST_UID_OTHER = 'test-uid-other' +@pytest.fixture(autouse=True) +def _stub_phone_call_plan_guards(monkeypatch): + monkeypatch.setattr('routers.phone_calls.check_call_access', MagicMock()) + monkeypatch.setattr( + 'routers.phone_calls.get_quota_snapshot', + MagicMock(return_value=SimpleNamespace(has_access=True, is_paid=True, max_duration_seconds=None)), + ) + monkeypatch.setattr('routers.phone_calls.check_destination_allowed', MagicMock()) + + def _make_app(): app = FastAPI() app.include_router(router) @@ -260,3 +345,19 @@ def test_twiml_success(mock_db, mock_check, mock_sig, client): body = resp.text assert '' in body assert '+15559876543' in body + + +@patch('routers.phone_calls.validate_twilio_signature', return_value=True) +@patch('routers.phone_calls.check_caller_id_verified', return_value=True) +@patch('routers.phone_calls.phone_call_usage_db') +@patch('routers.phone_calls.phone_calls_db') +def test_twiml_free_tier_counts_successful_call(mock_db, mock_usage_db, mock_check, mock_sig, client, monkeypatch): + mock_db.get_primary_phone_number.return_value = {'phone_number': '+15551234567'} + snapshot = SimpleNamespace(has_access=True, is_paid=False, max_duration_seconds=None) + monkeypatch.setattr('routers.phone_calls.get_quota_snapshot', MagicMock(return_value=snapshot)) + + resp = client.post('/v1/phone/twiml', data={'To': '+15559876543', 'From': f'client:{TEST_UID}', 'CallId': 'C1'}) + + assert resp.status_code == 200 + mock_usage_db.increment_current_month.assert_called_once_with(TEST_UID) + assert '' in resp.text