Coverage for app/backend/src/couchers/phone/sms.py: 100%

22 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +0000

1from typing import cast 

2 

3import boto3 

4import luhn 

5 

6from couchers import crypto 

7from couchers.config import config 

8from couchers.db import session_scope 

9from couchers.models import SMS 

10 

11 

12def generate_random_code() -> str: 

13 """Return a random 6-digit string with correct Luhn checksum""" 

14 return cast(str, luhn.append(crypto.generate_random_5digit_string())) 

15 

16 

17def looks_like_a_code(string: str) -> bool: 

18 return len(string) == 6 and string.isdigit() and luhn.verify(string) 

19 

20 

21def format_message(token: str) -> str: 

22 return f"{token} is your Couchers.org verification code. If you did not request this, please ignore this message. Best, the Couchers.org team." 

23 

24 

25def send_sms(number: str, message: str) -> str: 

26 """Send SMS to a E.164 formatted phone number with leading +. Return "success" on 

27 success, "unsupported operator" on unsupported operator, and any other 

28 string for any other error.""" 

29 

30 assert len(message) <= 140, "Message too long" 

31 

32 sns = boto3.client("sns") 

33 sender_id = config.SMS_SENDER_ID 

34 

35 response = sns.publish( 

36 PhoneNumber=number, 

37 Message=message, 

38 MessageAttributes={ 

39 "AWS.SNS.SMS.SMSType": {"DataType": "String", "StringValue": "Transactional"}, 

40 "AWS.SNS.SMS.SenderID": {"DataType": "String", "StringValue": sender_id}, 

41 }, 

42 ) 

43 

44 message_id = response["MessageId"] 

45 

46 with session_scope() as session: 

47 session.add( 

48 SMS( 

49 message_id=message_id, 

50 number=number, 

51 message=message, 

52 sms_sender_id=sender_id, 

53 ) 

54 ) 

55 

56 return "success"