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

mozilla / fx-private-relay / cafee54a-4333-4df8-8efd-a806f6de7814

25 Sep 2025 04:40PM UTC coverage: 88.855% (-0.03%) from 88.88%
cafee54a-4333-4df8-8efd-a806f6de7814

Pull #5903

circleci

groovecoder
for MPP-4415 feat(ci): add workflow to build and publish API docs to GitHub Pages
Pull Request #5903: for MPP-4415 feat(ci): add workflow to build and publish API docs to GitHub Pages

2916 of 3923 branches covered (74.33%)

Branch coverage included in aggregate %.

18044 of 19666 relevant lines covered (91.75%)

11.42 hits per line

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

98.88
/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

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

18
from privaterelay.tests.utils import omit_markus_logs
1✔
19

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

31

32
pytestmark = pytest.mark.skipif(
1✔
33
    not settings.PHONES_ENABLED, reason="PHONES_ENABLED is False"
34
)
35

36

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

42

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

48

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

73

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

81

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

98

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

108

109
@pytest.fixture(autouse=True)
1✔
110
def phone_user(db):
1✔
111
    return make_phone_test_user()
1✔
112

113

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

121

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

135

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

152

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

161

162
def test_create_second_realphone_for_user_raises_exception(
1✔
163
    phone_user, mock_twilio_client
164
):
165
    RealPhone.objects.create(user=phone_user, verified=True, number="+12223334444")
1✔
166
    mock_twilio_client.messages.create.assert_called_once()
1✔
167
    mock_twilio_client.reset_mock()
1✔
168

169
    with pytest.raises(BadRequest):
1✔
170
        RealPhone.objects.create(user=phone_user, number="+12223335555")
1✔
171
    mock_twilio_client.messages.assert_not_called()
1✔
172

173

174
def test_create_realphone_deletes_expired_unverified_records(
1✔
175
    phone_user, mock_twilio_client
176
):
177
    # create an expired unverified record
178
    number = "+12223334444"
1✔
179
    RealPhone.objects.create(
1✔
180
        user=phone_user,
181
        number=number,
182
        verified=False,
183
        verification_sent_date=(
184
            datetime.now(UTC)
185
            - timedelta(0, 60 * settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE + 1)
186
        ),
187
    )
188
    expired_verification_records = RealPhone.expired_objects.filter(number=number)
1✔
189
    assert len(expired_verification_records) >= 1
1✔
190
    mock_twilio_client.messages.create.assert_called_once()
1✔
191

192
    # now try to create the new record
193
    RealPhone.objects.create(user=baker.make(User), number=number)
1✔
194
    expired_verification_records = RealPhone.expired_objects.filter(number=number)
1✔
195
    assert len(expired_verification_records) == 0
1✔
196
    mock_twilio_client.messages.create.assert_called()
1✔
197

198

199
def test_mark_realphone_verified_sets_verified_and_date(phone_user):
1✔
200
    real_phone = RealPhone.objects.create(user=phone_user, verified=False)
1✔
201
    real_phone.mark_verified()
1✔
202
    assert real_phone.verified
1✔
203
    assert real_phone.verified_date
1✔
204

205

206
def test_create_relaynumber_without_realphone_raises_error(
1✔
207
    phone_user, mock_twilio_client
208
):
209
    with pytest.raises(ValidationError) as exc_info:
1✔
210
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
211
    assert exc_info.value.message == "User does not have a verified real phone."
1✔
212
    mock_twilio_client.messages.create.assert_not_called()
1✔
213
    mock_twilio_client.incoming_phone_numbers.create.assert_not_called()
1✔
214

215

216
def test_create_relaynumber_when_user_already_has_one_raises_error(
1✔
217
    phone_user, mock_twilio_client
218
):
219
    mock_messages_create = mock_twilio_client.messages.create
1✔
220
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
221

222
    real_phone = "+12223334444"
1✔
223
    RealPhone.objects.create(user=phone_user, verified=True, number=real_phone)
1✔
224
    mock_messages_create.assert_called_once()
1✔
225
    mock_messages_create.reset_mock()
1✔
226

227
    relay_number = "+19998887777"
1✔
228
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
229

230
    mock_number_create.assert_called_once()
1✔
231
    call_kwargs = mock_number_create.call_args.kwargs
1✔
232
    assert call_kwargs["phone_number"] == relay_number
1✔
233
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
234
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
235

236
    mock_messages_create.assert_called_once()
1✔
237
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
238
    assert "Welcome" in call_kwargs["body"]
1✔
239
    assert call_kwargs["to"] == real_phone
1✔
240
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
241

242
    mock_number_create.reset_mock()
1✔
243
    mock_messages_create.reset_mock()
1✔
244
    second_relay_number = "+14445556666"
1✔
245
    with pytest.raises(ValidationError) as exc_info:
1✔
246
        RelayNumber.objects.create(user=phone_user, number=second_relay_number)
1✔
247
    assert exc_info.value.message == "User can have only one relay number."
1✔
248
    mock_number_create.assert_not_called()
1✔
249
    mock_messages_create.assert_not_called()
1✔
250

251
    # Creating RelayNumber with same number is also an error
252
    with pytest.raises(ValidationError) as exc_info:
1✔
253
        RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
254
    assert exc_info.value.message == "User can have only one relay number."
1✔
255
    mock_number_create.assert_not_called()
1✔
256
    mock_messages_create.assert_not_called()
1✔
257

258

259
def test_create_duplicate_relaynumber_raises_error(phone_user, mock_twilio_client):
1✔
260
    mock_messages_create = mock_twilio_client.messages.create
1✔
261
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
262

263
    real_phone = "+12223334444"
1✔
264
    RealPhone.objects.create(user=phone_user, verified=True, number=real_phone)
1✔
265
    mock_messages_create.assert_called_once()
1✔
266
    mock_messages_create.reset_mock()
1✔
267

268
    relay_number = "+19998887777"
1✔
269
    RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
270

271
    mock_number_create.assert_called_once()
1✔
272
    call_kwargs = mock_number_create.call_args.kwargs
1✔
273
    assert call_kwargs["phone_number"] == relay_number
1✔
274
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
275
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
276

277
    mock_messages_create.assert_called_once()
1✔
278
    mock_number_create.reset_mock()
1✔
279
    mock_messages_create.reset_mock()
1✔
280

281
    second_user = make_phone_test_user()
1✔
282
    second_phone = "+15553334444"
1✔
283
    RealPhone.objects.create(user=second_user, verified=True, number=second_phone)
1✔
284
    mock_messages_create.assert_called_once()
1✔
285
    mock_messages_create.reset_mock()
1✔
286

287
    with pytest.raises(ValidationError) as exc_info:
1✔
288
        RelayNumber.objects.create(user=second_user, number=relay_number)
1✔
289
    assert exc_info.value.message == "This number is already claimed."
1✔
290
    mock_number_create.assert_not_called()
1✔
291
    mock_messages_create.assert_not_called()
1✔
292

293

294
@pytest.fixture
1✔
295
def real_phone_us(phone_user, mock_twilio_client):
1✔
296
    """Create a US-based RealPhone for phone_user, with a reset twilio_client."""
297
    real_phone = RealPhone.objects.create(
1✔
298
        user=phone_user,
299
        number="+12223334444",
300
        verified=True,
301
        verification_sent_date=datetime.now(UTC),
302
    )
303
    mock_twilio_client.messages.create.assert_called_once()
1✔
304
    mock_twilio_client.messages.create.reset_mock()
1✔
305
    return real_phone
1✔
306

307

308
def test_create_relaynumber_creates_twilio_incoming_number_and_sends_welcome(
1✔
309
    phone_user, real_phone_us, mock_twilio_client, settings, twilio_number_sid
310
):
311
    """A successful relay phone creation sends a welcome message."""
312
    relay_number = "+19998887777"
1✔
313
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
314

315
    mock_twilio_client.incoming_phone_numbers.create.assert_called_once_with(
1✔
316
        phone_number=relay_number,
317
        sms_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
318
        voice_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
319
    )
320
    mock_services = mock_twilio_client.messaging.v1.services
1✔
321
    mock_services.assert_called_once_with(settings.TWILIO_MESSAGING_SERVICE_SID[0])
1✔
322
    mock_services.return_value.phone_numbers.create.assert_called_once_with(
1✔
323
        phone_number_sid=twilio_number_sid
324
    )
325

326
    mock_messages_create = mock_twilio_client.messages.create
1✔
327
    mock_messages_create.assert_called_once()
1✔
328
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
329
    assert "Welcome" in call_kwargs["body"]
1✔
330
    assert call_kwargs["to"] == real_phone_us.number
1✔
331
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
332

333

334
def test_create_relaynumber_with_two_real_numbers(
1✔
335
    phone_user, mock_twilio_client, settings, twilio_number_sid
336
):
337
    """A user with a second unverified RealPhone is OK."""
338
    RealPhone.objects.create(user=phone_user, number="+12223334444", verified=False)
1✔
339
    phone2 = RealPhone.objects.create(
1✔
340
        user=phone_user, number="+12223335555", verified=False
341
    )
342
    phone2.mark_verified()
1✔
343
    mock_twilio_client.reset_mock()
1✔
344

345
    relay_number = "+19998887777"
1✔
346
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
347

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

359
    mock_messages_create = mock_twilio_client.messages.create
1✔
360
    mock_messages_create.assert_called_once()
1✔
361
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
362
    assert "Welcome" in call_kwargs["body"]
1✔
363
    assert call_kwargs["to"] == phone2.number
1✔
364
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
365

366

367
def test_create_relaynumber_already_registered_with_service(
1✔
368
    phone_user, real_phone_us, mock_twilio_client, caplog, settings, twilio_number_sid
369
):
370
    """
371
    It is OK if the relay phone is already registered with a messaging service.
372

373
    This is not likely in production, since relay phone acquisition and registration
374
    is a single step, but can happen when manually moving relay phones between users.
375
    """
376
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
377

378
    # Twilio responds that the phone number is already registered
379
    mock_services = mock_twilio_client.messaging.v1.services
1✔
380
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
381
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
382
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
383
        msg=(
384
            "Unable to create record:"
385
            " Phone Number or Short Code is already in the Messaging Service."
386
        ),
387
        method="POST",
388
        status=409,
389
        code=21710,
390
    )
391

392
    # Does not raise exception
393
    relay_number = "+19998887777"
1✔
394
    RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
395

396
    mock_twilio_client.incoming_phone_numbers.create.assert_called_once_with(
1✔
397
        phone_number=relay_number,
398
        sms_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
399
        voice_application_sid=settings.TWILIO_SMS_APPLICATION_SID,
400
    )
401
    mock_services.assert_called_once_with(twilio_service_sid)
1✔
402
    mock_messaging_number_create.assert_called_once_with(
1✔
403
        phone_number_sid=twilio_number_sid
404
    )
405
    mock_twilio_client.messages.create.assert_called_once()
1✔
406
    records = omit_markus_logs(caplog)
1✔
407
    assert len(records) == 1
1✔
408
    record = records[0]
1✔
409
    assert record.msg == "twilio_messaging_service"
1✔
410
    assert getattr(record, "code") == 21710
1✔
411

412

413
def test_create_relaynumber_fail_if_all_services_are_full(
1✔
414
    phone_user, real_phone_us, mock_twilio_client, settings, caplog, twilio_number_sid
415
):
416
    """If the Twilio Messaging Service pool is full, an exception is raised."""
417
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
418

419
    # Twilio responds that the pool is full
420
    mock_services = mock_twilio_client.messaging.v1.services
1✔
421
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
422
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
423
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
424
        msg=("Unable to create record: Number Pool size limit reached"),
425
        method="POST",
426
        status=412,
427
        code=21714,
428
    )
429

430
    # "Pool full" exception is raised
431
    with pytest.raises(Exception) as exc_info:
1✔
432
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
433
    assert (
1✔
434
        str(exc_info.value) == "All services in TWILIO_MESSAGING_SERVICE_SID are full"
435
    )
436

437
    mock_messaging_number_create.assert_called_once_with(
1✔
438
        phone_number_sid=twilio_number_sid
439
    )
440
    mock_twilio_client.messages.create.assert_not_called()
1✔
441
    records = omit_markus_logs(caplog)
1✔
442
    assert len(records) == 1
1✔
443
    record = records[0]
1✔
444
    assert record.msg == "twilio_messaging_service"
1✔
445
    assert getattr(record, "code") == 21714
1✔
446

447

448
def test_create_relaynumber_no_service(
1✔
449
    phone_user, real_phone_us, mock_twilio_client, settings, caplog
450
):
451
    """If no Twilio Messaging Service IDs are defined, registration is skipped."""
452
    settings.TWILIO_MESSAGING_SERVICE_SID = []
1✔
453

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

456
    mock_services = mock_twilio_client.messaging.v1.services
1✔
457
    mock_services.return_value.phone_numbers.create.assert_not_called()
1✔
458
    mock_twilio_client.messages.create.assert_called_once()
1✔
459
    records = omit_markus_logs(caplog)
1✔
460
    assert len(records) == 1
1✔
461
    record = records[0]
1✔
462
    assert record.msg == (
1✔
463
        "Skipping Twilio Messaging Service registration, since"
464
        " TWILIO_MESSAGING_SERVICE_SID is empty."
465
    )
466

467

468
def test_create_relaynumber_fallback_to_second_service(
1✔
469
    phone_user,
470
    real_phone_us,
471
    mock_twilio_client,
472
    settings,
473
    django_cache,
474
    caplog,
475
    twilio_number_sid,
476
):
477
    """The fallback messaging pool if the first is full."""
478
    twilio_service1_sid = f"MG{uuid4().hex}"
1✔
479
    twilio_service2_sid = f"MG{uuid4().hex}"
1✔
480
    settings.TWILIO_MESSAGING_SERVICE_SID = [twilio_service1_sid, twilio_service2_sid]
1✔
481
    django_cache.set("twilio_messaging_service_closed", "")
1✔
482

483
    # Twilio responds that pool 1 is full, pool 2 is OK
484
    mock_services = mock_twilio_client.messaging.v1.services
1✔
485
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
486
    mock_messaging_number_create.side_effect = [
1✔
487
        TwilioRestException(
488
            uri=f"/Services/{twilio_service1_sid}/PhoneNumbers",
489
            msg=("Unable to create record: Number Pool size limit reached"),
490
            method="POST",
491
            status=412,
492
            code=21714,
493
        ),
494
        None,
495
    ]
496

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

499
    mock_services.assert_has_calls(
1✔
500
        [
501
            call(twilio_service1_sid),
502
            call().phone_numbers.create(phone_number_sid=twilio_number_sid),
503
            call(twilio_service2_sid),
504
            call().phone_numbers.create(phone_number_sid=twilio_number_sid),
505
        ]
506
    )
507
    mock_twilio_client.messages.create.assert_called_once()
1✔
508

509
    assert django_cache.get("twilio_messaging_service_closed") == twilio_service1_sid
1✔
510
    records = omit_markus_logs(caplog)
1✔
511
    assert len(records) == 1
1✔
512
    record = records[0]
1✔
513
    assert record.msg == "twilio_messaging_service"
1✔
514
    assert getattr(record, "code") == 21714
1✔
515

516

517
def test_create_relaynumber_skip_known_full_service(
1✔
518
    phone_user,
519
    real_phone_us,
520
    mock_twilio_client,
521
    settings,
522
    django_cache,
523
    caplog,
524
    twilio_number_sid,
525
):
526
    """If a pool has been marked as full, it is skipped."""
527
    twilio_service1_sid = f"MG{uuid4().hex}"
1✔
528
    twilio_service2_sid = f"MG{uuid4().hex}"
1✔
529
    settings.TWILIO_MESSAGING_SERVICE_SID = [twilio_service1_sid, twilio_service2_sid]
1✔
530
    django_cache.set("twilio_messaging_service_closed", twilio_service1_sid)
1✔
531

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

534
    mock_services = mock_twilio_client.messaging.v1.services
1✔
535
    mock_services.assert_called_once_with(twilio_service2_sid)
1✔
536
    mock_services.return_value.phone_numbers.create.assert_called_once_with(
1✔
537
        phone_number_sid=twilio_number_sid
538
    )
539
    mock_twilio_client.messages.create.assert_called_once()
1✔
540
    assert django_cache.get("twilio_messaging_service_closed") == twilio_service1_sid
1✔
541
    assert len(omit_markus_logs(caplog)) == 0
1✔
542

543

544
def test_create_relaynumber_other_messaging_error_raised(
1✔
545
    phone_user,
546
    real_phone_us,
547
    mock_twilio_client,
548
    settings,
549
    caplog,
550
    django_cache,
551
    twilio_number_sid,
552
):
553
    """If adding to a pool raises a different error, it is skipped."""
554
    twilio_service_sid = settings.TWILIO_MESSAGING_SERVICE_SID[0]
1✔
555

556
    # Twilio responds that pool 1 is full, pool 2 is OK
557
    mock_services = mock_twilio_client.messaging.v1.services
1✔
558
    mock_messaging_number_create = mock_services.return_value.phone_numbers.create
1✔
559
    mock_messaging_number_create.side_effect = TwilioRestException(
1✔
560
        uri=f"/Services/{twilio_service_sid}/PhoneNumbers",
561
        msg=(
562
            "Unable to create record:"
563
            " Phone Number is associated with another Messaging Service"
564
        ),
565
        method="POST",
566
        status=409,
567
        code=21712,
568
    )
569

570
    with pytest.raises(TwilioRestException):
1✔
571
        RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
572

573
    mock_services.assert_called_once_with(twilio_service_sid)
1✔
574
    mock_messaging_number_create.assert_called_once_with(
1✔
575
        phone_number_sid=twilio_number_sid
576
    )
577
    mock_twilio_client.messages.create.assert_not_called()
1✔
578
    assert django_cache.get("twilio_messaging_service_closed") is None
1✔
579
    assert caplog.messages == ["twilio_messaging_service"]
1✔
580
    assert caplog.records[0].code == 21712
1✔
581

582

583
@pytest.fixture
1✔
584
def real_phone_ca(phone_user, mock_twilio_client):
1✔
585
    """Create a CA-based RealPhone for phone_user, with a reset twilio_client."""
586
    real_phone = RealPhone.objects.create(
1✔
587
        user=phone_user,
588
        number="+14035551234",
589
        verified=True,
590
        verification_sent_date=datetime.now(UTC),
591
        country_code="CA",
592
    )
593
    mock_twilio_client.messages.create.assert_called_once()
1✔
594
    mock_twilio_client.messages.create.reset_mock()
1✔
595
    return real_phone
1✔
596

597

598
def test_create_relaynumber_canada(
1✔
599
    phone_user, real_phone_ca, mock_twilio_client, twilio_number_sid
600
):
601
    relay_number = "+17805551234"
1✔
602
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
603
    assert relay_number_obj.country_code == "CA"
1✔
604

605
    mock_number_create = mock_twilio_client.incoming_phone_numbers.create
1✔
606
    mock_number_create.assert_called_once()
1✔
607
    call_kwargs = mock_number_create.call_args.kwargs
1✔
608
    assert call_kwargs["phone_number"] == relay_number
1✔
609
    assert call_kwargs["sms_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
610
    assert call_kwargs["voice_application_sid"] == settings.TWILIO_SMS_APPLICATION_SID
1✔
611

612
    # Omit Canadian numbers for US A2P 10DLC messaging service
613
    mock_twilio_client.messaging.v1.services.assert_not_called()
1✔
614

615
    # A welcome message is sent
616
    mock_messages_create = mock_twilio_client.messages.create
1✔
617
    mock_messages_create.assert_called_once()
1✔
618
    call_kwargs = mock_messages_create.call_args.kwargs
1✔
619
    assert "Welcome" in call_kwargs["body"]
1✔
620
    assert call_kwargs["to"] == real_phone_ca.number
1✔
621
    assert relay_number_obj.vcard_lookup_key in call_kwargs["media_url"][0]
1✔
622

623

624
def test_relaynumber_remaining_minutes_returns_properly_formats_remaining_seconds(
1✔
625
    phone_user, real_phone_us, mock_twilio_client
626
):
627
    relay_number = "+13045551234"
1✔
628
    relay_number_obj = RelayNumber.objects.create(user=phone_user, number=relay_number)
1✔
629

630
    # Freshly created RelayNumber should have 3000 seconds => 50 minutes
631
    assert relay_number_obj.remaining_minutes == 50
1✔
632

633
    # After receiving calls remaining_minutes property should return the rounded down
634
    # to a positive integer
635
    relay_number_obj.remaining_seconds = 522
1✔
636
    relay_number_obj.save()
1✔
637
    assert relay_number_obj.remaining_minutes == 8
1✔
638

639
    # If more call time is spent than allotted (negative remaining_seconds),
640
    # the remaining_minutes property should return zero
641
    relay_number_obj.remaining_seconds = -522
1✔
642
    relay_number_obj.save()
1✔
643
    assert relay_number_obj.remaining_minutes == 0
1✔
644

645

646
def test_suggested_numbers_bad_request_for_user_without_real_phone(
1✔
647
    phone_user, mock_twilio_client
648
):
649
    with pytest.raises(BadRequest):
1✔
650
        suggested_numbers(phone_user)
1✔
651
    mock_twilio_client.available_phone_numbers.assert_not_called()
1✔
652

653

654
def test_suggested_numbers_bad_request_for_user_who_already_has_number(
1✔
655
    phone_user, real_phone_us, mock_twilio_client
656
):
657
    RelayNumber.objects.create(user=phone_user, number="+19998887777")
1✔
658
    with pytest.raises(BadRequest):
1✔
659
        suggested_numbers(phone_user)
1✔
660
    mock_twilio_client.available_phone_numbers.assert_not_called()
1✔
661

662

663
def test_suggested_numbers(phone_user, real_phone_us, mock_twilio_client):
1✔
664
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
665
    mock_twilio_client.available_phone_numbers = Mock(
1✔
666
        return_value=Mock(local=Mock(list=mock_list))
667
    )
668

669
    suggested_numbers(phone_user)
1✔
670
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
671
    assert available_numbers_calls == [call("US")]
1✔
672
    assert mock_list.call_args_list == [
1✔
673
        call(contains="+1222333****", limit=10),
674
        call(contains="+122233***44", limit=10),
675
        call(contains="+12223******", limit=10),
676
        call(contains="+1***3334444", limit=10),
677
        call(contains="+1222*******", limit=10),
678
        call(limit=10),
679
    ]
680

681

682
def test_suggested_numbers_ca(phone_user, mock_twilio_client):
1✔
683
    real_phone = "+14035551234"
1✔
684
    RealPhone.objects.create(
1✔
685
        user=phone_user, verified=True, number=real_phone, country_code="CA"
686
    )
687
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
688
    mock_twilio_client.available_phone_numbers = Mock(
1✔
689
        return_value=Mock(local=Mock(list=mock_list))
690
    )
691

692
    suggested_numbers(phone_user)
1✔
693
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
694
    assert available_numbers_calls == [call("CA")]
1✔
695
    assert mock_list.call_args_list == [
1✔
696
        call(contains="+1403555****", limit=10),
697
        call(contains="+140355***34", limit=10),
698
        call(contains="+14035******", limit=10),
699
        call(contains="+1***5551234", limit=10),
700
        call(contains="+1403*******", limit=10),
701
        call(limit=10),
702
    ]
703

704

705
def test_location_numbers(mock_twilio_client):
1✔
706
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
707
    mock_twilio_client.available_phone_numbers = Mock(
1✔
708
        return_value=(Mock(local=Mock(list=mock_list)))
709
    )
710

711
    location_numbers("Miami, FL")
1✔
712

713
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
714
    assert available_numbers_calls == [call("US")]
1✔
715
    assert mock_list.call_args_list == [call(in_locality="Miami, FL", limit=10)]
1✔
716

717

718
def test_area_code_numbers(mock_twilio_client):
1✔
719
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
720
    mock_twilio_client.available_phone_numbers = Mock(
1✔
721
        return_value=(Mock(local=Mock(list=mock_list)))
722
    )
723

724
    area_code_numbers("918")
1✔
725

726
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
727
    assert available_numbers_calls == [call("US")]
1✔
728
    assert mock_list.call_args_list == [call(area_code="918", limit=10)]
1✔
729

730

731
def test_save_store_phone_log_no_relay_number_does_nothing() -> None:
1✔
732
    user = make_phone_test_user()
1✔
733
    user.profile.store_phone_log = True
1✔
734
    user.profile.save()
1✔
735

736
    user.profile.refresh_from_db()
1✔
737
    assert user.profile.store_phone_log
1✔
738

739
    user.profile.store_phone_log = False
1✔
740
    user.profile.save()
1✔
741
    assert not user.profile.store_phone_log
1✔
742

743

744
def test_save_store_phone_log_true_doesnt_delete_data() -> None:
1✔
745
    user = make_phone_test_user()
1✔
746
    baker.make(RealPhone, user=user, verified=True)
1✔
747
    relay_number = baker.make(RelayNumber, user=user)
1✔
748
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
749
    user.profile.store_phone_log = True
1✔
750
    user.profile.save()
1✔
751

752
    inbound_contact.refresh_from_db()
1✔
753
    assert inbound_contact
1✔
754

755

756
def _setup_phone_user_for_last_engagment(phone_user):
1✔
757
    add_verified_realphone_to_user(phone_user)
1✔
758
    relay_number = RelayNumber.objects.create(user=phone_user, number="+12223334444")
1✔
759

760
    # Get initial last_engagement
761
    initial_last_engagement = phone_user.profile.last_engagement
1✔
762
    return relay_number, initial_last_engagement
1✔
763

764

765
def test_relaynumber_save_updates_last_engagement(phone_user):
1✔
766
    """
767
    Test that updating specific RelayNumber fields triggers last_engagement update.
768
    """
769
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
770
        phone_user
771
    )
772

773
    # Update one of the tracked fields
774
    relay_number.calls_forwarded += 1
1✔
775
    relay_number.save()
1✔
776

777
    # Check if last_engagement was updated
778
    phone_user.profile.refresh_from_db()
1✔
779
    assert phone_user.profile.last_engagement is not None
1✔
780

781
    if initial_last_engagement:
1!
782
        assert phone_user.profile.last_engagement > initial_last_engagement
×
783

784

785
def test_relaynumber_save_no_update_when_other_fields_change(phone_user):
1✔
786
    """
787
    Test that updating fields NOT in the tracked list does NOT update last_engagement.
788
    """
789
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
790
        phone_user
791
    )
792

793
    # Update a field that is NOT in the tracked list
794
    relay_number.remaining_seconds -= 10
1✔
795
    relay_number.save()
1✔
796

797
    # Ensure last_engagement was NOT updated
798
    phone_user.profile.refresh_from_db()
1✔
799
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
800

801

802
def test_relaynumber_create_does_not_trigger_last_engagement(phone_user):
1✔
803
    """
804
    Test that creating a new RelayNumber does NOT trigger last_engagement update.
805
    """
806
    add_verified_realphone_to_user(phone_user)
1✔
807
    initial_last_engagement = phone_user.profile.last_engagement
1✔
808

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

811
    # Ensure last_engagement was NOT updated
812
    phone_user.profile.refresh_from_db()
1✔
813
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
814

815

816
def test_multiple_relaynumber_updates_trigger_last_engagement_once(phone_user):
1✔
817
    """
818
    Test that multiple updates to a tracked field still updates last_engagement.
819
    """
820
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
821
        phone_user
822
    )
823

824
    # Update multiple tracked fields
825
    relay_number.calls_forwarded += 1
1✔
826
    relay_number.calls_blocked += 1
1✔
827
    relay_number.texts_forwarded += 1
1✔
828
    relay_number.save()
1✔
829

830
    # Check if last_engagement was updated
831
    phone_user.profile.refresh_from_db()
1✔
832

833
    assert phone_user.profile.last_engagement is not None
1✔
834

835
    if initial_last_engagement:
1!
836
        assert phone_user.profile.last_engagement > initial_last_engagement
×
837

838

839
def test_save_store_phone_log_false_deletes_data() -> None:
1✔
840
    user = make_phone_test_user()
1✔
841
    baker.make(RealPhone, user=user, verified=True)
1✔
842
    relay_number = baker.make(RelayNumber, user=user)
1✔
843
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
844
    user.profile.store_phone_log = False
1✔
845
    user.profile.save()
1✔
846

847
    with pytest.raises(InboundContact.DoesNotExist):
1✔
848
        inbound_contact.refresh_from_db()
1✔
849

850

851
def test_get_last_text_sender_returning_None():
1✔
852
    user = make_phone_test_user()
1✔
853
    baker.make(RealPhone, user=user, verified=True)
1✔
854
    relay_number = baker.make(RelayNumber, user=user)
1✔
855

856
    assert get_last_text_sender(relay_number) is None
1✔
857

858

859
def test_get_last_text_sender_returning_one():
1✔
860
    user = make_phone_test_user()
1✔
861
    baker.make(RealPhone, user=user, verified=True)
1✔
862
    relay_number = baker.make(RelayNumber, user=user)
1✔
863
    inbound_contact = baker.make(
1✔
864
        InboundContact, relay_number=relay_number, last_inbound_type="text"
865
    )
866

867
    assert get_last_text_sender(relay_number) == inbound_contact
1✔
868

869

870
def test_get_last_text_sender_lots_of_inbound_returns_one():
1✔
871
    user = make_phone_test_user()
1✔
872
    baker.make(RealPhone, user=user, verified=True)
1✔
873
    relay_number = baker.make(RelayNumber, user=user)
1✔
874
    baker.make(
1✔
875
        InboundContact,
876
        relay_number=relay_number,
877
        last_inbound_type="call",
878
        last_inbound_date=datetime.now(UTC) - timedelta(days=4),
879
    )
880
    baker.make(
1✔
881
        InboundContact,
882
        relay_number=relay_number,
883
        last_inbound_type="text",
884
        last_inbound_date=datetime.now(UTC) - timedelta(days=3),
885
    )
886
    baker.make(
1✔
887
        InboundContact,
888
        relay_number=relay_number,
889
        last_inbound_type="call",
890
        last_inbound_date=datetime.now(UTC) - timedelta(days=2),
891
    )
892
    baker.make(
1✔
893
        InboundContact,
894
        relay_number=relay_number,
895
        last_inbound_type="text",
896
        last_inbound_date=datetime.now(UTC) - timedelta(days=1),
897
    )
898
    inbound_contact = baker.make(
1✔
899
        InboundContact,
900
        relay_number=relay_number,
901
        last_inbound_type="text",
902
        last_inbound_date=datetime.now(UTC),
903
    )
904

905
    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