Coverage for src/tests/test_verification.py: 100%
169 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
1from unittest.mock import Mock, patch
3import grpc
4import pytest
5from google.protobuf import empty_pb2
7import couchers.phone.sms
8from couchers import errors
9from couchers.config import config
10from couchers.crypto import random_hex
11from couchers.db import session_scope
12from couchers.models import SMS, User
13from couchers.sql import couchers_select as select
14from couchers.utils import now
15from proto import account_pb2, api_pb2
16from tests.test_fixtures import ( # noqa
17 account_session,
18 api_session,
19 db,
20 generate_user,
21 notifications_session,
22 process_jobs,
23 push_collector,
24 testconfig,
25)
28@pytest.fixture(autouse=True)
29def _(testconfig):
30 pass
33def test_ChangePhone(db, monkeypatch, push_collector):
34 user, token = generate_user()
35 user_id = user.id
37 with account_session(token) as account:
38 res = account.GetAccountInfo(empty_pb2.Empty())
39 assert res.phone == ""
41 monkeypatch.setattr(couchers.phone.sms, "send_sms", pytest.fail)
43 # Try with a too long number
44 with pytest.raises(grpc.RpcError) as e:
45 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+4670174060666666"))
46 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
48 # try to see if one digit too much is caught before attempting to send sms
49 with pytest.raises(grpc.RpcError) as e:
50 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+467017406066"))
51 assert e.value.code() == grpc.StatusCode.UNIMPLEMENTED
53 # Test with operator not supported by SMS backend
54 def deny_operator(phone, message):
55 assert phone == "+46701740605"
56 return "unsupported operator"
58 monkeypatch.setattr(couchers.phone.sms, "send_sms", deny_operator)
60 with pytest.raises(grpc.RpcError) as e:
61 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740605"))
62 assert e.value.code() == grpc.StatusCode.UNIMPLEMENTED
64 # Test with successfully sent SMS
65 def succeed(phone, message):
66 assert phone == "+46701740605"
67 return "success"
69 push_collector.assert_user_has_count(user_id, 0)
71 monkeypatch.setattr(couchers.phone.sms, "send_sms", succeed)
73 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740605"))
75 with session_scope() as session:
76 user = session.execute(select(User).where(User.id == user_id)).scalar_one()
77 assert user.phone == "+46701740605"
78 assert len(user.phone_verification_token) == 6
80 process_jobs()
81 push_collector.assert_user_has_single_matching(
82 user_id,
83 title="Phone verification started",
84 body="You started phone number verification with the number +46 70 174 06 05.",
85 )
87 # Phone number should show up but not be verified in your profile settings
88 res = account.GetAccountInfo(empty_pb2.Empty())
89 assert res.phone == "+46701740605"
90 assert not res.phone_verified
92 # Remove phone number
93 account.ChangePhone(account_pb2.ChangePhoneReq(phone=""))
95 with session_scope() as session:
96 user = session.execute(select(User).where(User.id == user_id)).scalar_one()
97 assert user.phone is None
98 assert user.phone_verification_token is None
101def test_ChangePhone_ratelimit(db, monkeypatch):
102 user, token = generate_user()
103 user_id = user.id
104 with account_session(token) as account:
106 def succeed(phone, message):
107 return "success"
109 monkeypatch.setattr(couchers.phone.sms, "send_sms", succeed)
111 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740605"))
113 with pytest.raises(grpc.RpcError) as e:
114 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740606"))
115 assert e.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
117 # Check that an earlier phone number/verification status is still saved
118 with session_scope() as session:
119 user = session.execute(select(User).where(User.id == user_id)).scalar_one()
120 assert user.phone == "+46701740605"
121 assert len(user.phone_verification_token) == 6
124def test_VerifyPhone(push_collector):
125 user, token = generate_user()
126 user_id = user.id
127 with account_session(token) as account, api_session(token) as api:
128 with pytest.raises(grpc.RpcError) as e:
129 account.VerifyPhone(account_pb2.VerifyPhoneReq(token="123455"))
130 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
132 res = api.GetUser(api_pb2.GetUserReq(user=str(user_id)))
133 assert res.verification == 0.0
135 with session_scope() as session:
136 user = session.execute(select(User).where(User.id == user_id)).scalar_one()
137 user.phone = "+46701740605"
138 user.phone_verification_token = "111112"
139 user.phone_verification_sent = now()
141 account.VerifyPhone(account_pb2.VerifyPhoneReq(token="111112"))
143 process_jobs()
144 push_collector.assert_user_has_single_matching(
145 user_id,
146 title="Phone successfully verified",
147 body="Your phone was successfully verified as +46 70 174 06 05 on Couchers.org.",
148 )
150 res = api.GetUser(api_pb2.GetUserReq(user=str(user_id)))
151 assert res.verification == 1.0
153 # Phone number should finally show up on in your profile settings
154 res = account.GetAccountInfo(empty_pb2.Empty())
155 assert res.phone == "+46701740605"
158def test_VerifyPhone_antibrute():
159 user, token = generate_user()
160 user_id = user.id
161 with account_session(token) as account, api_session(token) as api:
162 with session_scope() as session:
163 user = session.execute(select(User).where(User.id == user_id)).scalar_one()
164 user.phone_verification_token = "111112"
165 user.phone_verification_sent = now()
166 user.phone = "+46701740605"
168 for _ in range(10):
169 with pytest.raises(grpc.RpcError) as e:
170 account.VerifyPhone(account_pb2.VerifyPhoneReq(token="123455"))
171 if e.value.code() != grpc.StatusCode.NOT_FOUND:
172 break
173 assert e.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
176def test_phone_uniqueness(monkeypatch):
177 user1, token1 = generate_user()
178 user2, token2 = generate_user()
179 with account_session(token1) as account1, account_session(token2) as account2:
181 def succeed(phone, message):
182 return "success"
184 monkeypatch.setattr(couchers.phone.sms, "send_sms", succeed)
186 account1.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740605"))
187 with session_scope() as session:
188 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
189 token = user.phone_verification_token
190 account1.VerifyPhone(account_pb2.VerifyPhoneReq(token=token))
191 res = account1.GetAccountInfo(empty_pb2.Empty())
192 assert res.phone == "+46701740605"
193 assert res.phone_verified
195 # Let user2 steal user1:s phone number
197 account2.ChangePhone(account_pb2.ChangePhoneReq(phone="+46701740605"))
199 res = account1.GetAccountInfo(empty_pb2.Empty())
200 assert res.phone == "+46701740605"
201 assert res.phone_verified
203 res = account2.GetAccountInfo(empty_pb2.Empty())
204 assert res.phone == "+46701740605"
205 assert not res.phone_verified
207 with session_scope() as session:
208 user = session.execute(select(User).where(User.id == user2.id)).scalar_one()
209 token = user.phone_verification_token
210 account2.VerifyPhone(account_pb2.VerifyPhoneReq(token=token))
212 # number gets wiped when it's stolen
213 res = account1.GetAccountInfo(empty_pb2.Empty())
214 assert not res.phone
215 assert not res.phone_verified
217 res = account2.GetAccountInfo(empty_pb2.Empty())
218 assert res.phone == "+46701740605"
219 assert res.phone_verified
222def test_send_sms(db, monkeypatch):
223 new_config = config.copy()
224 new_config["ENABLE_SMS"] = True
225 new_config["SMS_SENDER_ID"] = "CouchersOrg"
226 monkeypatch.setattr(couchers.phone.sms, "config", new_config)
228 msg_id = random_hex()
230 with patch("couchers.phone.sms.boto3") as mock:
231 sns = Mock()
232 sns.publish.return_value = {"MessageId": msg_id}
233 mock.client.return_value = sns
235 assert couchers.phone.sms.send_sms("+46701740605", "Testing SMS message") == "success"
237 mock.client.assert_called_once_with("sns")
238 sns.publish.assert_called_once_with(
239 PhoneNumber="+46701740605",
240 Message="Testing SMS message",
241 MessageAttributes={
242 "AWS.SNS.SMS.SMSType": {"DataType": "String", "StringValue": "Transactional"},
243 "AWS.SNS.SMS.SenderID": {"DataType": "String", "StringValue": "CouchersOrg"},
244 },
245 )
247 with session_scope() as session:
248 sms = session.execute(select(SMS)).scalar_one()
249 assert sms.message_id == msg_id
250 assert sms.sms_sender_id == "CouchersOrg"
251 assert sms.number == "+46701740605"
252 assert sms.message == "Testing SMS message"
255def test_send_sms_disabled(db):
256 assert not config["ENABLE_SMS"]
257 assert couchers.phone.sms.send_sms("+46701740605", "Testing SMS message") == "SMS not enabled."
260def test_sms_verification_no_donation():
261 user, token = generate_user(has_donated=False)
262 with account_session(token) as account:
263 with pytest.raises(grpc.RpcError) as e:
264 account.ChangePhone(account_pb2.ChangePhoneReq(phone="+467017406066"))
265 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
266 assert e.value.details() == errors.NOT_DONATED