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

mozilla / fx-private-relay / 79269098-5b53-4282-9874-44a10a23d292

11 Mar 2025 06:50PM CUT coverage: 85.121% (-0.002%) from 85.123%
79269098-5b53-4282-9874-44a10a23d292

push

circleci

groovecoder
fix MPP-4106: refactor(phone): update last_engagement on phone operations

2436 of 3567 branches covered (68.29%)

Branch coverage included in aggregate %.

43 of 45 new or added lines in 2 files covered. (95.56%)

2 existing lines in 1 file now uncovered.

17089 of 19371 relevant lines covered (88.22%)

9.86 hits per line

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

97.81
/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 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✔
169
    number = "+12223334444"
×
170
    iq_number = iq_fmt(number)
×
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

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

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_ca(phone_user, mock_twilio_client):
1✔
712
    real_phone = "+14035551234"
1✔
713
    RealPhone.objects.create(
1✔
714
        user=phone_user, verified=True, number=real_phone, country_code="CA"
715
    )
716
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
717
    mock_twilio_client.available_phone_numbers = Mock(
1✔
718
        return_value=Mock(local=Mock(list=mock_list))
719
    )
720

721
    suggested_numbers(phone_user)
1✔
722
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
723
    assert available_numbers_calls == [call("CA")]
1✔
724
    assert mock_list.call_args_list == [
1✔
725
        call(contains="+1403555****", limit=10),
726
        call(contains="+140355***34", limit=10),
727
        call(contains="+14035******", limit=10),
728
        call(contains="+1***5551234", limit=10),
729
        call(contains="+1403*******", limit=10),
730
        call(limit=10),
731
    ]
732

733

734
def test_location_numbers(mock_twilio_client):
1✔
735
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
736
    mock_twilio_client.available_phone_numbers = Mock(
1✔
737
        return_value=(Mock(local=Mock(list=mock_list)))
738
    )
739

740
    location_numbers("Miami, FL")
1✔
741

742
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
743
    assert available_numbers_calls == [call("US")]
1✔
744
    assert mock_list.call_args_list == [call(in_locality="Miami, FL", limit=10)]
1✔
745

746

747
def test_area_code_numbers(mock_twilio_client):
1✔
748
    mock_list = Mock(return_value=[Mock() for i in range(5)])
1✔
749
    mock_twilio_client.available_phone_numbers = Mock(
1✔
750
        return_value=(Mock(local=Mock(list=mock_list)))
751
    )
752

753
    area_code_numbers("918")
1✔
754

755
    available_numbers_calls = mock_twilio_client.available_phone_numbers.call_args_list
1✔
756
    assert available_numbers_calls == [call("US")]
1✔
757
    assert mock_list.call_args_list == [call(area_code="918", limit=10)]
1✔
758

759

760
def test_save_store_phone_log_no_relay_number_does_nothing() -> None:
1✔
761
    user = make_phone_test_user()
1✔
762
    user.profile.store_phone_log = True
1✔
763
    user.profile.save()
1✔
764

765
    user.profile.refresh_from_db()
1✔
766
    assert user.profile.store_phone_log
1✔
767

768
    user.profile.store_phone_log = False
1✔
769
    user.profile.save()
1✔
770
    assert not user.profile.store_phone_log
1✔
771

772

773
def test_save_store_phone_log_true_doesnt_delete_data() -> None:
1✔
774
    user = make_phone_test_user()
1✔
775
    baker.make(RealPhone, user=user, verified=True)
1✔
776
    relay_number = baker.make(RelayNumber, user=user)
1✔
777
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
778
    user.profile.store_phone_log = True
1✔
779
    user.profile.save()
1✔
780

781
    inbound_contact.refresh_from_db()
1✔
782
    assert inbound_contact
1✔
783

784

785
def _setup_phone_user_for_last_engagment(phone_user):
1✔
786
    add_verified_realphone_to_user(phone_user)
1✔
787
    relay_number = RelayNumber.objects.create(user=phone_user, number="+12223334444")
1✔
788

789
    # Get initial last_engagement
790
    initial_last_engagement = phone_user.profile.last_engagement
1✔
791
    return relay_number, initial_last_engagement
1✔
792

793

794
def test_relaynumber_save_updates_last_engagement(phone_user):
1✔
795
    """
796
    Test that updating specific RelayNumber fields triggers last_engagement update.
797
    """
798
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
799
        phone_user
800
    )
801

802
    # Update one of the tracked fields
803
    relay_number.calls_forwarded += 1
1✔
804
    relay_number.save()
1✔
805

806
    # Check if last_engagement was updated
807
    phone_user.profile.refresh_from_db()
1✔
808
    assert phone_user.profile.last_engagement is not None
1✔
809

810
    if initial_last_engagement:
1!
NEW
811
        assert phone_user.profile.last_engagement > initial_last_engagement
×
812

813

814
def test_relaynumber_save_no_update_when_other_fields_change(phone_user):
1✔
815
    """
816
    Test that updating fields NOT in the tracked list does NOT update last_engagement.
817
    """
818
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
819
        phone_user
820
    )
821

822
    # Update a field that is NOT in the tracked list
823
    relay_number.remaining_seconds -= 10
1✔
824
    relay_number.save()
1✔
825

826
    # Ensure last_engagement was NOT updated
827
    phone_user.profile.refresh_from_db()
1✔
828
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
829

830

831
def test_relaynumber_create_does_not_trigger_last_engagement(phone_user):
1✔
832
    """
833
    Test that creating a new RelayNumber does NOT trigger last_engagement update.
834
    """
835
    add_verified_realphone_to_user(phone_user)
1✔
836
    initial_last_engagement = phone_user.profile.last_engagement
1✔
837

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

840
    # Ensure last_engagement was NOT updated
841
    phone_user.profile.refresh_from_db()
1✔
842
    assert phone_user.profile.last_engagement == initial_last_engagement
1✔
843

844

845
def test_multiple_relaynumber_updates_trigger_last_engagement_once(phone_user):
1✔
846
    """
847
    Test that multiple updates to a tracked field still updates last_engagement.
848
    """
849
    relay_number, initial_last_engagement = _setup_phone_user_for_last_engagment(
1✔
850
        phone_user
851
    )
852

853
    # Update multiple tracked fields
854
    relay_number.calls_forwarded += 1
1✔
855
    relay_number.calls_blocked += 1
1✔
856
    relay_number.texts_forwarded += 1
1✔
857
    relay_number.save()
1✔
858

859
    # Check if last_engagement was updated
860
    phone_user.profile.refresh_from_db()
1✔
861

862
    assert phone_user.profile.last_engagement is not None
1✔
863

864
    if initial_last_engagement:
1!
NEW
865
        assert phone_user.profile.last_engagement > initial_last_engagement
×
866

867

868
def test_save_store_phone_log_false_deletes_data() -> None:
1✔
869
    user = make_phone_test_user()
1✔
870
    baker.make(RealPhone, user=user, verified=True)
1✔
871
    relay_number = baker.make(RelayNumber, user=user)
1✔
872
    inbound_contact = baker.make(InboundContact, relay_number=relay_number)
1✔
873
    user.profile.store_phone_log = False
1✔
874
    user.profile.save()
1✔
875

876
    with pytest.raises(InboundContact.DoesNotExist):
1✔
877
        inbound_contact.refresh_from_db()
1✔
878

879

880
def test_get_last_text_sender_returning_None():
1✔
881
    user = make_phone_test_user()
1✔
882
    baker.make(RealPhone, user=user, verified=True)
1✔
883
    relay_number = baker.make(RelayNumber, user=user)
1✔
884

885
    assert get_last_text_sender(relay_number) is None
1✔
886

887

888
def test_get_last_text_sender_returning_one():
1✔
889
    user = make_phone_test_user()
1✔
890
    baker.make(RealPhone, user=user, verified=True)
1✔
891
    relay_number = baker.make(RelayNumber, user=user)
1✔
892
    inbound_contact = baker.make(
1✔
893
        InboundContact, relay_number=relay_number, last_inbound_type="text"
894
    )
895

896
    assert get_last_text_sender(relay_number) == inbound_contact
1✔
897

898

899
def test_get_last_text_sender_lots_of_inbound_returns_one():
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
    baker.make(
1✔
904
        InboundContact,
905
        relay_number=relay_number,
906
        last_inbound_type="call",
907
        last_inbound_date=datetime.now(UTC) - timedelta(days=4),
908
    )
909
    baker.make(
1✔
910
        InboundContact,
911
        relay_number=relay_number,
912
        last_inbound_type="text",
913
        last_inbound_date=datetime.now(UTC) - timedelta(days=3),
914
    )
915
    baker.make(
1✔
916
        InboundContact,
917
        relay_number=relay_number,
918
        last_inbound_type="call",
919
        last_inbound_date=datetime.now(UTC) - timedelta(days=2),
920
    )
921
    baker.make(
1✔
922
        InboundContact,
923
        relay_number=relay_number,
924
        last_inbound_type="text",
925
        last_inbound_date=datetime.now(UTC) - timedelta(days=1),
926
    )
927
    inbound_contact = baker.make(
1✔
928
        InboundContact,
929
        relay_number=relay_number,
930
        last_inbound_type="text",
931
        last_inbound_date=datetime.now(UTC),
932
    )
933

934
    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