• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / 3f809551-6712-425b-b278-bf1cf7d34ed4

19 Sep 2025 06:01PM UTC coverage: 88.138% (-0.7%) from 88.863%
3f809551-6712-425b-b278-bf1cf7d34ed4

Pull #5885

circleci

joeherm
fix(twilio): Add error handling for flaky Twilio calls
Pull Request #5885: fix(twilio): Add error handling for erroring Twilio calls

2925 of 3955 branches covered (73.96%)

Branch coverage included in aggregate %.

42 of 42 new or added lines in 2 files covered. (100.0%)

116 existing lines in 7 files now uncovered.

18199 of 20012 relevant lines covered (90.94%)

11.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.89
/phones/tests/models_tests.py
1
import random
1✔
2
from collections.abc import Iterator
1✔
3
from datetime import UTC, datetime, timedelta
1✔
4
from types import SimpleNamespace
1✔
5
from unittest.mock import Mock, call, patch
1✔
6
from uuid import uuid4
1✔
7

8
from django.conf import settings
1✔
9
from django.contrib.auth.models import User
1✔
10
from django.core.cache import cache
1✔
11
from django.core.exceptions import BadRequest, ValidationError
1✔
12
from django.test import override_settings
1✔
13

14
import pytest
1✔
15
import responses
1✔
16
from allauth.socialaccount.models import SocialAccount, SocialToken
1✔
17
from model_bakery import baker
1✔
18
from twilio.base.exceptions import TwilioException, TwilioRestException
1✔
19

20
from privaterelay.tests.utils import omit_markus_logs
1✔
21

22
if settings.PHONES_ENABLED:
1!
23
    from ..models import (
1✔
24
        InboundContact,
25
        RealPhone,
26
        RelayNumber,
27
        area_code_numbers,
28
        get_last_text_sender,
29
        iq_fmt,
30
        location_numbers,
31
        suggested_numbers,
32
    )
33

34

35
pytestmark = pytest.mark.skipif(
1✔
36
    not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False"
37
)
38

39

40
@pytest.fixture(autouse=True)
1✔
41
def test_settings(settings):
1✔
42
    settings.TWILIO_MESSAGING_SERVICE_SID = [f"MG{uuid4().hex}"]
1✔
43
    return settings
1✔
44

45

46
@pytest.fixture
1✔
47
def twilio_number_sid():
1✔
48
    """A Twilio Incoming Number ID"""
49
    return f"PN{uuid4().hex}"
1✔
50

51

52
@pytest.fixture(autouse=True)
1✔
53
def mock_twilio_client(twilio_number_sid: str) -> Iterator[Mock]:
1✔
54
    """Mock PhonesConfig with a mock twilio client"""
55
    with patch(
1✔
56
        "phones.apps.PhonesConfig.twilio_client",
57
        spec_set=[
58
            "available_phone_numbers",
59
            "incoming_phone_numbers",
60
            "messages",
61
            "messaging",
62
        ],
63
    ) as mock_twilio_client:
64
        mock_twilio_client.available_phone_numbers = Mock(spec_set=[])
1✔
65
        mock_twilio_client.incoming_phone_numbers = Mock(spec_set=["create"])
1✔
66
        mock_twilio_client.incoming_phone_numbers.create = Mock(
1✔
67
            spec_set=[], return_value=SimpleNamespace(sid=twilio_number_sid)
68
        )
69
        mock_twilio_client.messages = Mock(spec_set=["create"])
1✔
70
        mock_twilio_client.messages.create = Mock(spec_set=[])
1✔
71
        mock_twilio_client.messaging = Mock(spec_set=["v1"])
1✔
72
        mock_twilio_client.messaging.v1 = Mock(spec_set=["services"])
1✔
73
        mock_twilio_client.messaging.v1.services = Mock(spec_set=[])
1✔
74
        yield mock_twilio_client
1✔
75

76

77
def make_phone_test_user() -> User:
1✔
78
    phone_user = baker.make(User, email="phone_user@example.com")
1✔
79
    phone_user.profile.date_subscribed = datetime.now(tz=UTC) - timedelta(days=15)
1✔
80
    phone_user.profile.save()
1✔
81
    upgrade_test_user_to_phone(phone_user)
1✔
82
    return phone_user
1✔
83

84

85
def upgrade_test_user_to_phone(user):
1✔
86
    random_sub = random.choice(settings.SUBSCRIPTIONS_WITH_PHONE)
1✔
87
    account: SocialAccount = baker.make(
1✔
88
        SocialAccount,
89
        user=user,
90
        provider="fxa",
91
        uid=str(uuid4()).replace("-", ""),
92
        extra_data={"avatar": "avatar.png", "subscriptions": [random_sub]},
93
    )
94
    baker.make(
1✔
95
        SocialToken,
96
        account=account,
97
        expires_at=datetime.now(UTC) + timedelta(1),
98
    )
99
    return user
1✔
100

101

102
def add_verified_realphone_to_user(phone_user: User) -> "RealPhone":
1✔
103
    number = "+12223334444"
1✔
104
    return RealPhone.objects.create(
1✔
105
        user=phone_user,
106
        number=number,
107
        verification_sent_date=datetime.now(UTC),
108
        verified=True,
109
    )
110

111

112
@pytest.fixture(autouse=True)
1✔
113
def phone_user(db):
1✔
114
    return make_phone_test_user()
1✔
115

116

117
@pytest.fixture
1✔
118
def django_cache():
1✔
119
    """Return a cleared Django cache as a fixture."""
120
    cache.clear()
1✔
121
    yield cache
1✔
122
    cache.clear()
1✔
123

124

125
def test_realphone_pending_objects_includes_new(phone_user):
1✔
126
    number = "+12223334444"
1✔
127
    real_phone = RealPhone.objects.create(
1✔
128
        user=phone_user,
129
        number=number,
130
        verification_sent_date=datetime.now(UTC),
131
    )
132
    assert RealPhone.pending_objects.exists_for_number(number)
1✔
133
    recent_phone = RealPhone.recent_objects.get_for_user_number_and_verification_code(
1✔
134
        phone_user, number, real_phone.verification_code
135
    )
136
    assert recent_phone.id == real_phone.id
1✔
137

138

139
def test_realphone_pending_objects_excludes_old(phone_user):
1✔
140
    number = "+12223334444"
1✔
141
    real_phone = RealPhone.objects.create(
1✔
142
        user=phone_user,
143
        number=number,
144
        verification_sent_date=(
145
            datetime.now(UTC)
146
            - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE + 1)
147
        ),
148
    )
149
    assert not RealPhone.pending_objects.exists_for_number(number)
1✔
150
    with pytest.raises(RealPhone.DoesNotExist):
1✔
151
        RealPhone.recent_objects.get_for_user_number_and_verification_code(
1✔
152
            phone_user, number, real_phone.verification_code
153
        )
154

155

156
def test_create_realphone_creates_twilio_message(phone_user, mock_twilio_client):
1✔
157
    number = "+12223334444"
1✔
158
    RealPhone.objects.create(user=phone_user, verified=True, number=number)
1✔
159
    mock_twilio_client.messages.create.assert_called_once()
1✔
160
    call_kwargs = mock_twilio_client.messages.create.call_args.kwargs
1✔
161
    assert call_kwargs["to"] == number
1✔
162
    assert "verification code" in call_kwargs["body"]
1✔
163

164

165
@override_settings(IQ_FOR_VERIFICATION=True)
1✔
166
@responses.activate
1✔
167
@pytest.mark.skipif(not settings.IQ_ENABLED, reason="IQ_ENABLED is false")
1✔
168
def test_create_realphone_creates_iq_message(phone_user):
1✔
UNCOV
169
    number = "+12223334444"
×
UNCOV
170
    iq_number = iq_fmt(number)
×
UNCOV
171
    resp = responses.add(
×
172
        responses.POST,
173
        settings.IQ_PUBLISH_MESSAGE_URL,
174
        status=200,
175
        match=[
176
            responses.matchers.json_params_matcher(
177
                {
178
                    "to": [iq_number],
179
                    "from": settings.IQ_MAIN_NUMBER,
180
                },
181
                strict_match=False,
182
            )
183
        ],
184
    )
185

UNCOV
186
    RealPhone.objects.create(user=phone_user, verified=True, number=number)
×
187

UNCOV
188
    assert resp.call_count == 1
×
189

190

191
def test_create_second_realphone_for_user_raises_exception(
1✔
192
    phone_user, mock_twilio_client
193
):
194
    RealPhone.objects.create(user=phone_user, verified=True, number="+12223334444")
1✔
195
    mock_twilio_client.messages.create.assert_called_once()
1✔
196
    mock_twilio_client.reset_mock()
1✔
197

198
    with pytest.raises(BadRequest):
1✔
199
        RealPhone.objects.create(user=phone_user, number="+12223335555")
1✔
200
    mock_twilio_client.messages.assert_not_called()
1✔
201

202

203
def test_create_realphone_deletes_expired_unverified_records(
1✔
204
    phone_user, mock_twilio_client
205
):
206
    # create an expired unverified record
207
    number = "+12223334444"
1✔
208
    RealPhone.objects.create(
1✔
209
        user=phone_user,
210
        number=number,
211
        verified=False,
212
        verification_sent_date=(
213
            datetime.now(UTC)
214
            - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE + 1)
215
        ),
216
    )
217
    expired_verification_records = RealPhone.expired_objects.filter(number=number)
1✔
218
    assert len(expired_verification_records) >= 1
1✔
219
    mock_twilio_client.messages.create.assert_called_once()
1✔
220

221
    # now try to create the new record
222
    RealPhone.objects.create(user=baker.make(User), number=number)
1✔
223
    expired_verification_records = RealPhone.expired_objects.filter(number=number)
1✔
224
    assert len(expired_verification_records) == 0
1✔
225
    mock_twilio_client.messages.create.assert_called()
1✔
226

227

228
def test_mark_realphone_verified_sets_verified_and_date(phone_user):
1✔
229
    real_phone = RealPhone.objects.create(user=phone_user, verified=False)
1✔
230
    real_phone.mark_verified()
1✔
231
    assert real_phone.verified
1✔
232
    assert real_phone.verified_date
1✔
233

234

235
def test_create_relaynumber_without_realphone_raises_error(
1✔
236
    phone_user, mock_twilio_client
237
):
238
    with pytest.raises(ValidationError) as exc_info:
1✔
239
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
240
    assert exc_info.value.message == "User does not have a verified real phone."
1✔
241
    mock_twilio_client.messages.create.assert_not_called()
1✔
242
    mock_twilio_client.incoming_phone_numbers.create.assert_not_called()
1✔
243

244

245
def test_create_relaynumber_when_user_already_has_one_raises_error(
1✔
246
    phone_user, mock_twilio_client
247
):
248
    mock_messages_create = mock_twilio_client.messages.create
1✔
249
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
250

251
    real_phone = "+12223334444"
1✔
252
    RealPhone.objects.create(user=phone_user, verified=True, number=real_phone)
1✔
253
    mock_messages_create.assert_called_once()
1✔
254
    mock_messages_create.reset_mock()
1✔
255

256
    relay_number = "+19998887777"
1✔
257
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
258

259
    mock_number_create.assert_called_once()
1✔
260
    call_kwargs = mock_number_create.call_args.kwargs
1✔
261
    assert call_kwargs["phone_number"] == relay_number
1✔
262
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
263
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
264

265
    mock_messages_create.assert_called_once()
1✔
266
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
267
    assert "Welcome" in call_kwargs["body"]
1✔
268
    assert call_kwargs["to"] == real_phone
1✔
269
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
270

271
    mock_number_create.reset_mock()
1✔
272
    mock_messages_create.reset_mock()
1✔
273
    second_relay_number = "+14445556666"
1✔
274
    with pytest.raises(ValidationError) as exc_info:
1✔
275
        RelayNumber.objects.create(user=phone_user, number=second_relay_number)
1✔
276
    assert exc_info.value.message == "User can have only one relay number."
1✔
277
    mock_number_create.assert_not_called()
1✔
278
    mock_messages_create.assert_not_called()
1✔
279

280
    # Creating RelayNumber with same number is also an error
281
    with pytest.raises(ValidationError) as exc_info:
1✔
282
        RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
283
    assert exc_info.value.message == "User can have only one relay number."
1✔
284
    mock_number_create.assert_not_called()
1✔
285
    mock_messages_create.assert_not_called()
1✔
286

287

288
def test_create_duplicate_relaynumber_raises_error(phone_user, mock_twilio_client):
1✔
289
    mock_messages_create = mock_twilio_client.messages.create
1✔
290
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
291

292
    real_phone = "+12223334444"
1✔
293
    RealPhone.objects.create(user=phone_user, verified=True, number=real_phone)
1✔
294
    mock_messages_create.assert_called_once()
1✔
295
    mock_messages_create.reset_mock()
1✔
296

297
    relay_number = "+19998887777"
1✔
298
    RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
299

300
    mock_number_create.assert_called_once()
1✔
301
    call_kwargs = mock_number_create.call_args.kwargs
1✔
302
    assert call_kwargs["phone_number"] == relay_number
1✔
303
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
304
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
305

306
    mock_messages_create.assert_called_once()
1✔
307
    mock_number_create.reset_mock()
1✔
308
    mock_messages_create.reset_mock()
1✔
309

310
    second_user = make_phone_test_user()
1✔
311
    second_phone = "+15553334444"
1✔
312
    RealPhone.objects.create(user=second_user, verified=True, number=second_phone)
1✔
313
    mock_messages_create.assert_called_once()
1✔
314
    mock_messages_create.reset_mock()
1✔
315

316
    with pytest.raises(ValidationError) as exc_info:
1✔
317
        RelayNumber.objects.create(user=second_user, number=relay_number)
1✔
318
    assert exc_info.value.message == "This number is already claimed."
1✔
319
    mock_number_create.assert_not_called()
1✔
320
    mock_messages_create.assert_not_called()
1✔
321

322

323
@pytest.fixture
1✔
324
def real_phone_us(phone_user, mock_twilio_client):
1✔
325
    """Create a US-based RealPhone for phone_user, with a reset twilio_client."""
326
    real_phone = RealPhone.objects.create(
1✔
327
        user=phone_user,
328
        number="+12223334444",
329
        verified=True,
330
        verification_sent_date=datetime.now(UTC),
331
    )
332
    mock_twilio_client.messages.create.assert_called_once()
1✔
333
    mock_twilio_client.messages.create.reset_mock()
1✔
334
    return real_phone
1✔
335

336

337
def test_create_relaynumber_creates_twilio_incoming_number_and_sends_welcome(
1✔
338
    phone_user, real_phone_us, mock_twilio_client, settings, twilio_number_sid
339
):
340
    """A successful relay phone creation sends a welcome message."""
341
    relay_number = "+19998887777"
1✔
342
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
343

344
    mock_twilio_client.incoming_phone_numbers.create.assert_called_once_with(
1✔
345
        phone_number=relay_number,
346
        sms_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
347
        voice_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
348
    )
349
    mock_services = mock_twilio_client.messaging.v1.services
1✔
350
    mock_services.assert_called_once_with(settings.TWILIO_MESSAGING_SERVICE_SID[0])
1✔
351
    mock_services.return_value.phone_numbers.create.assert_called_once_with(
1✔
352
        phone_number_sid=twilio_number_sid
353
    )
354

355
    mock_messages_create = mock_twilio_client.messages.create
1✔
356
    mock_messages_create.assert_called_once()
1✔
357
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
358
    assert "Welcome" in call_kwargs["body"]
1✔
359
    assert call_kwargs["to"] == real_phone_us.number
1✔
360
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
361

362

363
def test_create_relaynumber_with_two_real_numbers(
1✔
364
    phone_user, mock_twilio_client, settings, twilio_number_sid
365
):
366
    """A user with a second unverified RealPhone is OK."""
367
    RealPhone.objects.create(user=phone_user, number="+12223334444", verified=False)
1✔
368
    phone2 = RealPhone.objects.create(
1✔
369
        user=phone_user, number="+12223335555", verified=False
370
    )
371
    phone2.mark_verified()
1✔
372
    mock_twilio_client.reset_mock()
1✔
373

374
    relay_number = "+19998887777"
1✔
375
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
376

377
    mock_twilio_client.incoming_phone_numbers.create.assert_called_once_with(
1✔
378
        phone_number=relay_number,
379
        sms_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
380
        voice_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
381
    )
382
    mock_services = mock_twilio_client.messaging.v1.services
1✔
383
    mock_services.assert_called_once_with(settings.TWILIO_MESSAGING_SERVICE_SID[0])
1✔
384
    mock_services.return_value.phone_numbers.create.assert_called_once_with(
1✔
385
        phone_number_sid=twilio_number_sid
386
    )
387

388
    mock_messages_create = mock_twilio_client.messages.create
1✔
389
    mock_messages_create.assert_called_once()
1✔
390
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
391
    assert "Welcome" in call_kwargs["body"]
1✔
392
    assert call_kwargs["to"] == phone2.number
1✔
393
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
394

395

396
def test_create_relaynumber_already_registered_with_service(
1✔
397
    phone_user, real_phone_us, mock_twilio_client, caplog, settings, twilio_number_sid
398
):
399
    """
400
    It is OK if the relay phone is already registered with a messaging service.
401

402
    This is not likely in production, since relay phone acquisition and registration
403
    is a single step, but can happen when manually moving relay phones between users.
404
    """
405
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
406

407
    # Twilio responds that the phone number is already registered
408
    mock_services = mock_twilio_client.messaging.v1.services
1✔
409
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
410
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
411
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
412
        msg=(
413
            "Unable to create record:"
414
            " Phone Number or Short Code is already in the Messaging Service."
415
        ),
416
        method="POST",
417
        status=409,
418
        code=21710,
419
    )
420

421
    # Does not raise exception
422
    relay_number = "+19998887777"
1✔
423
    RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
424

425
    mock_twilio_client.incoming_phone_numbers.create.assert_called_once_with(
1✔
426
        phone_number=relay_number,
427
        sms_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
428
        voice_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
429
    )
430
    mock_services.assert_called_once_with(twilio_service_sid)
1✔
431
    mock_messaging_number_create.assert_called_once_with(
1✔
432
        phone_number_sid=twilio_number_sid
433
    )
434
    mock_twilio_client.messages.create.assert_called_once()
1✔
435
    records = omit_markus_logs(caplog)
1✔
436
    assert len(records) == 1
1✔
437
    record = records[0]
1✔
438
    assert record.msg == "twilio_messaging_service"
1✔
439
    assert getattr(record, "code") == 21710
1✔
440

441

442
def test_create_relaynumber_fail_if_all_services_are_full(
1✔
443
    phone_user, real_phone_us, mock_twilio_client, settings, caplog, twilio_number_sid
444
):
445
    """If the Twilio Messaging Service pool is full, an exception is raised."""
446
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
447

448
    # Twilio responds that the pool is full
449
    mock_services = mock_twilio_client.messaging.v1.services
1✔
450
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
451
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
452
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
453
        msg=("Unable to create record: Number Pool size limit reached"),
454
        method="POST",
455
        status=412,
456
        code=21714,
457
    )
458

459
    # "Pool full" exception is raised
460
    with pytest.raises(Exception) as exc_info:
1✔
461
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
462
    assert (
1✔
463
        str(exc_info.value) == "All services in TWILIO_MESSAGING_SERVICE_SID are full"
464
    )
465

466
    mock_messaging_number_create.assert_called_once_with(
1✔
467
        phone_number_sid=twilio_number_sid
468
    )
469
    mock_twilio_client.messages.create.assert_not_called()
1✔
470
    records = omit_markus_logs(caplog)
1✔
471
    assert len(records) == 1
1✔
472
    record = records[0]
1✔
473
    assert record.msg == "twilio_messaging_service"
1✔
474
    assert getattr(record, "code") == 21714
1✔
475

476

477
def test_create_relaynumber_no_service(
1✔
478
    phone_user, real_phone_us, mock_twilio_client, settings, caplog
479
):
480
    """If no Twilio Messaging Service IDs are defined, registration is skipped."""
481
    settings.TWILIO_MESSAGING_SERVICE_SID = []
1✔
482

483
    RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
484

485
    mock_services = mock_twilio_client.messaging.v1.services
1✔
486
    mock_services.return_value.phone_numbers.create.assert_not_called()
1✔
487
    mock_twilio_client.messages.create.assert_called_once()
1✔
488
    records = omit_markus_logs(caplog)
1✔
489
    assert len(records) == 1
1✔
490
    record = records[0]
1✔
491
    assert record.msg == (
1✔
492
        "Skipping Twilio Messaging Service registration, since"
493
        " TWILIO_MESSAGING_SERVICE_SID is empty."
494
    )
495

496

497
def test_create_relaynumber_fallback_to_second_service(
1✔
498
    phone_user,
499
    real_phone_us,
500
    mock_twilio_client,
501
    settings,
502
    django_cache,
503
    caplog,
504
    twilio_number_sid,
505
):
506
    """The fallback messaging pool if the first is full."""
507
    twilio_service1_sid = f"MG{uuid4().hex}"
1✔
508
    twilio_service2_sid = f"MG{uuid4().hex}"
1✔
509
    settings.TWILIO_MESSAGING_SERVICE_SID = [twilio_service1_sid, twilio_service2_sid]
1✔
510
    django_cache.set("twilio_messaging_service_closed", "")
1✔
511

512
    # Twilio responds that pool 1 is full, pool 2 is OK
513
    mock_services = mock_twilio_client.messaging.v1.services
1✔
514
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
515
    mock_messaging_number_create.side_effect = [
1✔
516
        TwilioRestException(
517
            uri=f"/Services/{twilio_service1_sid}/PhoneNumbers",
518
            msg=("Unable to create record: Number Pool size limit reached"),
519
            method="POST",
520
            status=412,
521
            code=21714,
522
        ),
523
        None,
524
    ]
525

526
    RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
527

528
    mock_services.assert_has_calls(
1✔
529
        [
530
            call(twilio_service1_sid),
531
            call().phone_numbers.create(phone_number_sid=twilio_number_sid),
532
            call(twilio_service2_sid),
533
            call().phone_numbers.create(phone_number_sid=twilio_number_sid),
534
        ]
535
    )
536
    mock_twilio_client.messages.create.assert_called_once()
1✔
537

538
    assert django_cache.get("twilio_messaging_service_closed") == twilio_service1_sid
1✔
539
    records = omit_markus_logs(caplog)
1✔
540
    assert len(records) == 1
1✔
541
    record = records[0]
1✔
542
    assert record.msg == "twilio_messaging_service"
1✔
543
    assert getattr(record, "code") == 21714
1✔
544

545

546
def test_create_relaynumber_skip_known_full_service(
1✔
547
    phone_user,
548
    real_phone_us,
549
    mock_twilio_client,
550
    settings,
551
    django_cache,
552
    caplog,
553
    twilio_number_sid,
554
):
555
    """If a pool has been marked as full, it is skipped."""
556
    twilio_service1_sid = f"MG{uuid4().hex}"
1✔
557
    twilio_service2_sid = f"MG{uuid4().hex}"
1✔
558
    settings.TWILIO_MESSAGING_SERVICE_SID = [twilio_service1_sid, twilio_service2_sid]
1✔
559
    django_cache.set("twilio_messaging_service_closed", twilio_service1_sid)
1✔
560

561
    RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
562

563
    mock_services = mock_twilio_client.messaging.v1.services
1✔
564
    mock_services.assert_called_once_with(twilio_service2_sid)
1✔
565
    mock_services.return_value.phone_numbers.create.assert_called_once_with(
1✔
566
        phone_number_sid=twilio_number_sid
567
    )
568
    mock_twilio_client.messages.create.assert_called_once()
1✔
569
    assert django_cache.get("twilio_messaging_service_closed") == twilio_service1_sid
1✔
570
    assert len(omit_markus_logs(caplog)) == 0
1✔
571

572

573
def test_create_relaynumber_other_messaging_error_raised(
1✔
574
    phone_user,
575
    real_phone_us,
576
    mock_twilio_client,
577
    settings,
578
    caplog,
579
    django_cache,
580
    twilio_number_sid,
581
):
582
    """If adding to a pool raises a different error, it is skipped."""
583
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
584

585
    # Twilio responds that pool 1 is full, pool 2 is OK
586
    mock_services = mock_twilio_client.messaging.v1.services
1✔
587
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
588
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
589
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
590
        msg=(
591
            "Unable to create record:"
592
            " Phone Number is associated with another Messaging Service"
593
        ),
594
        method="POST",
595
        status=409,
596
        code=21712,
597
    )
598

599
    with pytest.raises(TwilioRestException):
1✔
600
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
601

602
    mock_services.assert_called_once_with(twilio_service_sid)
1✔
603
    mock_messaging_number_create.assert_called_once_with(
1✔
604
        phone_number_sid=twilio_number_sid
605
    )
606
    mock_twilio_client.messages.create.assert_not_called()
1✔
607
    assert django_cache.get("twilio_messaging_service_closed") is None
1✔
608
    assert caplog.messages == ["twilio_messaging_service"]
1✔
609
    assert caplog.records[0].code == 21712
1✔
610

611

612
@pytest.fixture
1✔
613
def real_phone_ca(phone_user, mock_twilio_client):
1✔
614
    """Create a CA-based RealPhone for phone_user, with a reset twilio_client."""
615
    real_phone = RealPhone.objects.create(
1✔
616
        user=phone_user,
617
        number="+14035551234",
618
        verified=True,
619
        verification_sent_date=datetime.now(UTC),
620
        country_code="CA",
621
    )
622
    mock_twilio_client.messages.create.assert_called_once()
1✔
623
    mock_twilio_client.messages.create.reset_mock()
1✔
624
    return real_phone
1✔
625

626

627
def test_create_relaynumber_canada(
1✔
628
    phone_user, real_phone_ca, mock_twilio_client, twilio_number_sid
629
):
630
    relay_number = "+17805551234"
1✔
631
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
632
    assert relay_number_obj.country_code == "CA"
1✔
633

634
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
635
    mock_number_create.assert_called_once()
1✔
636
    call_kwargs = mock_number_create.call_args.kwargs
1✔
637
    assert call_kwargs["phone_number"] == relay_number
1✔
638
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
639
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
640

641
    # Omit Canadian numbers for US A2P 10DLC messaging service
642
    mock_twilio_client.messaging.v1.services.assert_not_called()
1✔
643

644
    # A welcome message is sent
645
    mock_messages_create = mock_twilio_client.messages.create
1✔
646
    mock_messages_create.assert_called_once()
1✔
647
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
648
    assert "Welcome" in call_kwargs["body"]
1✔
649
    assert call_kwargs["to"] == real_phone_ca.number
1✔
650
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
651

652

653
def test_relaynumber_remaining_minutes_returns_properly_formats_remaining_seconds(
1✔
654
    phone_user, real_phone_us, mock_twilio_client
655
):
656
    relay_number = "+13045551234"
1✔
657
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
658

659
    # Freshly created RelayNumber should have 3000 seconds => 50 minutes
660
    assert relay_number_obj.remaining_minutes == 50
1✔
661

662
    # After receiving calls remaining_minutes property should return the rounded down
663
    # to a positive integer
664
    relay_number_obj.remaining_seconds = 522
1✔
665
    relay_number_obj.save()
1✔
666
    assert relay_number_obj.remaining_minutes == 8
1✔
667

668
    # If more call time is spent than allotted (negative remaining_seconds),
669
    # the remaining_minutes property should return zero
670
    relay_number_obj.remaining_seconds = -522
1✔
671
    relay_number_obj.save()
1✔
672
    assert relay_number_obj.remaining_minutes == 0
1✔
673

674

675
def test_suggested_numbers_bad_request_for_user_without_real_phone(
1✔
676
    phone_user, mock_twilio_client
677
):
678
    with pytest.raises(BadRequest):
1✔
679
        suggested_numbers(phone_user)
1✔
680
    mock_twilio_client.available_phone_numbers.assert_not_called()
1✔
681

682

683
def test_suggested_numbers_bad_request_for_user_who_already_has_number(
1✔
684
    phone_user, real_phone_us, mock_twilio_client
685
):
686
    RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
687
    with pytest.raises(BadRequest):
1✔
688
        suggested_numbers(phone_user)
1✔
689
    mock_twilio_client.available_phone_numbers.assert_not_called()
1✔
690

691

692
def test_suggested_numbers(phone_user, real_phone_us, mock_twilio_client):
1✔
693
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
694
    mock_twilio_client.available_phone_numbers = Mock(
1✔
695
        return_value=Mock(local=Mock(list=mock_list))
696
    )
697

698
    suggested_numbers(phone_user)
1✔
699
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
700
    assert available_numbers_calls == [call("US")]
1✔
701
    assert mock_list.call_args_list == [
1✔
702
        call(contains="+1222333****", limit=10),
703
        call(contains="+122233***44", limit=10),
704
        call(contains="+12223******", limit=10),
705
        call(contains="+1***3334444", limit=10),
706
        call(contains="+1222*******", limit=10),
707
        call(limit=10),
708
    ]
709

710

711
def test_suggested_numbers_hotfix_error_test(
1✔
712
    phone_user, real_phone_us, mock_twilio_client
713
):
714
    # Special hotfix test case for error handling certain types of numbers, clean later
715
    def _twilioCall(contains=None, limit=None):
1✔
716
        if contains:
1✔
717
            # Any prefix will raise an error, only the most general fetch succeeds
718
            if contains == "+1***3334444":
1✔
719
                raise TwilioRestException(500, "/url", "Service Error 500")
1✔
720
            raise TwilioException("Unable to fetch page")
1✔
721
        return [Mock() for i in range(5)]
1✔
722

723
    mock_list = Mock(side_effect=_twilioCall)
1✔
724
    mock_twilio_client.available_phone_numbers = Mock(
1✔
725
        return_value=Mock(local=Mock(list=mock_list))
726
    )
727

728
    numbers = suggested_numbers(phone_user)
1✔
729
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
730
    assert available_numbers_calls == [call("US")]
1✔
731
    assert mock_list.call_args_list == [
1✔
732
        call(contains="+1222333****", limit=10),
733
        call(contains="+122233***44", limit=10),
734
        call(contains="+12223******", limit=10),
735
        call(contains="+1***3334444", limit=10),
736
        call(contains="+1222*******", limit=10),
737
        call(limit=10),
738
    ]
739
    assert len(numbers) == 5
1✔
740

741

742
def test_suggested_numbers_ca(phone_user, mock_twilio_client):
1✔
743
    real_phone = "+14035551234"
1✔
744
    RealPhone.objects.create(
1✔
745
        user=phone_user, verified=True, number=real_phone, country_code="CA"
746
    )
747
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
748
    mock_twilio_client.available_phone_numbers = Mock(
1✔
749
        return_value=Mock(local=Mock(list=mock_list))
750
    )
751

752
    suggested_numbers(phone_user)
1✔
753
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
754
    assert available_numbers_calls == [call("CA")]
1✔
755
    assert mock_list.call_args_list == [
1✔
756
        call(contains="+1403555****", limit=10),
757
        call(contains="+140355***34", limit=10),
758
        call(contains="+14035******", limit=10),
759
        call(contains="+1***5551234", limit=10),
760
        call(contains="+1403*******", limit=10),
761
        call(limit=10),
762
    ]
763

764

765
def test_location_numbers(mock_twilio_client):
1✔
766
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
767
    mock_twilio_client.available_phone_numbers = Mock(
1✔
768
        return_value=(Mock(local=Mock(list=mock_list)))
769
    )
770

771
    location_numbers("Miami, FL")
1✔
772

773
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
774
    assert available_numbers_calls == [call("US")]
1✔
775
    assert mock_list.call_args_list == [call(in_locality="Miami, FL", limit=10)]
1✔
776

777

778
def test_area_code_numbers(mock_twilio_client):
1✔
779
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
780
    mock_twilio_client.available_phone_numbers = Mock(
1✔
781
        return_value=(Mock(local=Mock(list=mock_list)))
782
    )
783

784
    area_code_numbers("918")
1✔
785

786
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
787
    assert available_numbers_calls == [call("US")]
1✔
788
    assert mock_list.call_args_list == [call(area_code="918", limit=10)]
1✔
789

790

791
def test_save_store_phone_log_no_relay_number_does_nothing() -> None:
1✔
792
    user = make_phone_test_user()
1✔
793
    user.profile.store_phone_log = True
1✔
794
    user.profile.save()
1✔
795

796
    user.profile.refresh_from_db()
1✔
797
    assert user.profile.store_phone_log
1✔
798

799
    user.profile.store_phone_log = False
1✔
800
    user.profile.save()
1✔
801
    assert not user.profile.store_phone_log
1✔
802

803

804
def test_save_store_phone_log_true_doesnt_delete_data() -> None:
1✔
805
    user = make_phone_test_user()
1✔
806
    baker.make(RealPhone, user=user, verified=True)
1✔
807
    relay_number = baker.make(RelayNumber, user=user)
1✔
808
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
809
    user.profile.store_phone_log = True
1✔
810
    user.profile.save()
1✔
811

812
    inbound_contact.refresh_from_db()
1✔
813
    assert inbound_contact
1✔
814

815

816
def _setup_phone_user_for_last_engagment(phone_user):
1✔
817
    add_verified_realphone_to_user(phone_user)
1✔
818
    relay_number = RelayNumber.objects.create(user=phone_user, number="+12223334444")
1✔
819

820
    # Get initial last_engagement
821
    initial_last_engagement = phone_user.profile.last_engagement
1✔
822
    return relay_number, initial_last_engagement
1✔
823

824

825
def test_relaynumber_save_updates_last_engagement(phone_user):
1✔
826
    """
827
    Test that updating specific RelayNumber fields triggers last_engagement update.
828
    """
829
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
830
        phone_user
831
    )
832

833
    # Update one of the tracked fields
834
    relay_number.calls_forwarded += 1
1✔
835
    relay_number.save()
1✔
836

837
    # Check if last_engagement was updated
838
    phone_user.profile.refresh_from_db()
1✔
839
    assert phone_user.profile.last_engagement is not None
1✔
840

841
    if initial_last_engagement:
1!
UNCOV
842
        assert phone_user.profile.last_engagement > initial_last_engagement
×
843

844

845
def test_relaynumber_save_no_update_when_other_fields_change(phone_user):
1✔
846
    """
847
    Test that updating fields NOT in the tracked list does NOT update last_engagement.
848
    """
849
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
850
        phone_user
851
    )
852

853
    # Update a field that is NOT in the tracked list
854
    relay_number.remaining_seconds -= 10
1✔
855
    relay_number.save()
1✔
856

857
    # Ensure last_engagement was NOT updated
858
    phone_user.profile.refresh_from_db()
1✔
859
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
860

861

862
def test_relaynumber_create_does_not_trigger_last_engagement(phone_user):
1✔
863
    """
864
    Test that creating a new RelayNumber does NOT trigger last_engagement update.
865
    """
866
    add_verified_realphone_to_user(phone_user)
1✔
867
    initial_last_engagement = phone_user.profile.last_engagement
1✔
868

869
    RelayNumber.objects.create(user=phone_user, number="+12223334444")
1✔
870

871
    # Ensure last_engagement was NOT updated
872
    phone_user.profile.refresh_from_db()
1✔
873
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
874

875

876
def test_multiple_relaynumber_updates_trigger_last_engagement_once(phone_user):
1✔
877
    """
878
    Test that multiple updates to a tracked field still updates last_engagement.
879
    """
880
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
881
        phone_user
882
    )
883

884
    # Update multiple tracked fields
885
    relay_number.calls_forwarded += 1
1✔
886
    relay_number.calls_blocked += 1
1✔
887
    relay_number.texts_forwarded += 1
1✔
888
    relay_number.save()
1✔
889

890
    # Check if last_engagement was updated
891
    phone_user.profile.refresh_from_db()
1✔
892

893
    assert phone_user.profile.last_engagement is not None
1✔
894

895
    if initial_last_engagement:
1!
UNCOV
896
        assert phone_user.profile.last_engagement > initial_last_engagement
×
897

898

899
def test_save_store_phone_log_false_deletes_data() -> None:
1✔
900
    user = make_phone_test_user()
1✔
901
    baker.make(RealPhone, user=user, verified=True)
1✔
902
    relay_number = baker.make(RelayNumber, user=user)
1✔
903
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
904
    user.profile.store_phone_log = False
1✔
905
    user.profile.save()
1✔
906

907
    with pytest.raises(InboundContact.DoesNotExist):
1✔
908
        inbound_contact.refresh_from_db()
1✔
909

910

911
def test_get_last_text_sender_returning_None():
1✔
912
    user = make_phone_test_user()
1✔
913
    baker.make(RealPhone, user=user, verified=True)
1✔
914
    relay_number = baker.make(RelayNumber, user=user)
1✔
915

916
    assert get_last_text_sender(relay_number) is None
1✔
917

918

919
def test_get_last_text_sender_returning_one():
1✔
920
    user = make_phone_test_user()
1✔
921
    baker.make(RealPhone, user=user, verified=True)
1✔
922
    relay_number = baker.make(RelayNumber, user=user)
1✔
923
    inbound_contact = baker.make(
1✔
924
        InboundContact, relay_number=relay_number, last_inbound_type="text"
925
    )
926

927
    assert get_last_text_sender(relay_number) == inbound_contact
1✔
928

929

930
def test_get_last_text_sender_lots_of_inbound_returns_one():
1✔
931
    user = make_phone_test_user()
1✔
932
    baker.make(RealPhone, user=user, verified=True)
1✔
933
    relay_number = baker.make(RelayNumber, user=user)
1✔
934
    baker.make(
1✔
935
        InboundContact,
936
        relay_number=relay_number,
937
        last_inbound_type="call",
938
        last_inbound_date=datetime.now(UTC) - timedelta(days=4),
939
    )
940
    baker.make(
1✔
941
        InboundContact,
942
        relay_number=relay_number,
943
        last_inbound_type="text",
944
        last_inbound_date=datetime.now(UTC) - timedelta(days=3),
945
    )
946
    baker.make(
1✔
947
        InboundContact,
948
        relay_number=relay_number,
949
        last_inbound_type="call",
950
        last_inbound_date=datetime.now(UTC) - timedelta(days=2),
951
    )
952
    baker.make(
1✔
953
        InboundContact,
954
        relay_number=relay_number,
955
        last_inbound_type="text",
956
        last_inbound_date=datetime.now(UTC) - timedelta(days=1),
957
    )
958
    inbound_contact = baker.make(
1✔
959
        InboundContact,
960
        relay_number=relay_number,
961
        last_inbound_type="text",
962
        last_inbound_date=datetime.now(UTC),
963
    )
964

965
    assert get_last_text_sender(relay_number) == inbound_contact
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc