3030from django .test .utils import override_settings
3131from django .urls import reverse
3232from django .utils import timezone
33+ from weblate .accounts .models import AuditLog
3334from weblate .auth .models import User
3435from weblate .billing .models import Billing , Invoice , Plan
3536from weblate .trans .models import Project
4445 pending_payments ,
4546 recurring_payments ,
4647)
48+ from wlhosted .integrations .views import (
49+ USER_ENSURE_RESPONSE_SALT ,
50+ USER_ENSURE_SALT ,
51+ USER_SYNC_RESPONSE_SALT ,
52+ USER_SYNC_SALT ,
53+ make_unique_username ,
54+ )
4755from wlhosted .payments .backends import get_backend
4856from wlhosted .payments .models import Customer , Payment
4957
@@ -97,7 +105,7 @@ def create_trial(self):
97105 bill = Billing .objects .create (state = Billing .STATE_TRIAL , plan = self .plan_b )
98106 bill .owners .add (self .user )
99107 project = Project .objects .create (name = "Project" , slug = "project" )
100- bill .projects . add (project )
108+ bill .add_project (project )
101109 project .add_user (self .user )
102110 return bill
103111
@@ -152,13 +160,28 @@ def test_queue_user_sync_refreshes_cursor(self) -> None:
152160 old_updated = timezone .now () - relativedelta (hours = 1 )
153161 UserSyncState .objects .filter (pk = sync_state .pk ).update (updated = old_updated )
154162
155- with patch ("wlhosted.integrations.tasks.notify_user_change.delay" ) as delay :
163+ with (
164+ patch ("wlhosted.integrations.tasks.notify_user_change.delay" ) as delay ,
165+ self .captureOnCommitCallbacks (execute = True ),
166+ ):
156167 queue_user_sync (self .user )
157168
158169 sync_state .refresh_from_db ()
159170 self .assertGreater (sync_state .updated , old_updated )
160171 delay .assert_called_once ()
161172
173+ @override_settings (PAYMENT_SECRET = TEST_PAYMENT_SECRET )
174+ def test_queue_user_sync_notifies_after_commit (self ) -> None :
175+ with (
176+ patch ("wlhosted.integrations.tasks.notify_user_change.delay" ) as delay ,
177+ self .captureOnCommitCallbacks (execute = False ) as callbacks ,
178+ ):
179+ queue_user_sync (self .user )
180+ delay .assert_not_called ()
181+
182+ self .assertEqual (len (callbacks ), 1 )
183+ delay .assert_not_called ()
184+
162185 @override_settings (PAYMENT_SECRET = "" )
163186 def test_queue_user_sync_no_payment_secret (self ) -> None :
164187 UserSyncState .objects .filter (user = self .user ).delete ()
@@ -197,7 +220,7 @@ def test_api_users(self) -> None:
197220 "payload" : dumps (
198221 {"since" : "" },
199222 key = TEST_PAYMENT_SECRET ,
200- salt = "weblate.user-sync" ,
223+ salt = USER_SYNC_SALT ,
201224 )
202225 },
203226 )
@@ -207,7 +230,7 @@ def test_api_users(self) -> None:
207230 payload = loads (
208231 response .json ()["payload" ],
209232 key = TEST_PAYMENT_SECRET ,
210- salt = "weblate.user-sync-response" ,
233+ salt = USER_SYNC_RESPONSE_SALT ,
211234 )
212235 self .assertIn (
213236 str (self .user .pk ), {user ["external_id" ] for user in payload ["users" ]}
@@ -221,7 +244,7 @@ def test_api_users_requires_post(self) -> None:
221244 "payload" : dumps (
222245 {"since" : "" },
223246 key = TEST_PAYMENT_SECRET ,
224- salt = "weblate.user-sync" ,
247+ salt = USER_SYNC_SALT ,
225248 )
226249 },
227250 )
@@ -252,7 +275,7 @@ def test_api_users_incremental(self) -> None:
252275 "payload" : dumps (
253276 {"since" : since .isoformat ()},
254277 key = TEST_PAYMENT_SECRET ,
255- salt = "weblate.user-sync" ,
278+ salt = USER_SYNC_SALT ,
256279 )
257280 },
258281 )
@@ -261,7 +284,7 @@ def test_api_users_incremental(self) -> None:
261284 payload = loads (
262285 response .json ()["payload" ],
263286 key = TEST_PAYMENT_SECRET ,
264- salt = "weblate.user-sync-response" ,
287+ salt = USER_SYNC_RESPONSE_SALT ,
265288 )
266289 self .assertEqual (
267290 [user ["external_id" ] for user in payload ["users" ]],
@@ -279,7 +302,7 @@ def test_api_users_invalid_cursor(self) -> None:
279302 "payload" : dumps (
280303 {"since" : since },
281304 key = TEST_PAYMENT_SECRET ,
282- salt = "weblate.user-sync" ,
305+ salt = USER_SYNC_SALT ,
283306 )
284307 },
285308 )
@@ -290,8 +313,177 @@ def test_api_users_invalid_cursor(self) -> None:
290313 def test_api_users_no_payment_secret (self ) -> None :
291314 response = self .client .post (
292315 reverse ("hosted-api-users" ),
293- {"payload" : dumps ({"since" : "" }, key = "" , salt = "weblate.user-sync" )},
316+ {"payload" : dumps ({"since" : "" }, key = "" , salt = USER_SYNC_SALT )},
317+ )
318+
319+ self .assertEqual (response .status_code , 400 )
320+
321+ @override_settings (
322+ ENABLE_HTTPS = True ,
323+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
324+ SITE_DOMAIN = "hosted.example.org" ,
325+ )
326+ def test_api_user_ensure_creates_user (self ) -> None :
327+ mail .outbox .clear ()
328+ with (
329+ patch ("wlhosted.integrations.tasks.notify_user_change.delay" ),
330+ self .captureOnCommitCallbacks (execute = True ),
331+ ):
332+ response = self .client .post (
333+ reverse ("hosted-api-user-ensure" ),
334+ {
335+ "payload" : dumps (
336+ {
337+ "email" : "created@example.org" ,
338+ "full_name" : "Created User" ,
339+ },
340+ key = TEST_PAYMENT_SECRET ,
341+ salt = USER_ENSURE_SALT ,
342+ )
343+ },
344+ )
345+
346+ self .assertEqual (response .status_code , 200 )
347+ user = User .objects .get (email = "created@example.org" )
348+ self .assertEqual (user .username , "created" )
349+ self .assertEqual (user .full_name , "Created User" )
350+ self .assertFalse (user .has_usable_password ())
351+ payload = loads (
352+ response .json ()["payload" ],
353+ key = TEST_PAYMENT_SECRET ,
354+ salt = USER_ENSURE_RESPONSE_SALT ,
355+ )
356+ self .assertTrue (payload ["created" ])
357+ self .assertEqual (payload ["user" ]["external_id" ], str (user .pk ))
358+
359+ audit = AuditLog .objects .get (user = user , activity = "external-create" )
360+ self .assertIn ("created externally from weblate.org" , audit .get_message ())
361+ self .assertEqual (len (mail .outbox ), 1 )
362+ self .assertIn (
363+ "Weblate.org has created this Hosted Weblate sign-in account" ,
364+ mail .outbox [0 ].body ,
294365 )
366+ self .assertIn ("purchased Weblate services" , mail .outbox [0 ].body )
367+ self .assertIn ("Set account password" , " " .join (mail .outbox [0 ].body .split ()))
368+ self .assertIn ("verification_code=" , mail .outbox [0 ].body )
369+ self .assertIn (
370+ f"https://hosted.example.org{ reverse ('social:complete' , args = ('email' ,))} " ,
371+ mail .outbox [0 ].body ,
372+ )
373+
374+ @override_settings (
375+ ENABLE_HTTPS = True ,
376+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
377+ SITE_DOMAIN = "hosted.example.org" ,
378+ )
379+ def test_api_user_ensure_uses_concurrently_created_user (self ) -> None :
380+ def create_concurrent_user (email : str ) -> str :
381+ User .objects .create_user (
382+ username = "created" ,
383+ email = email ,
384+ password = TESTPASSWORD ,
385+ full_name = "Created Elsewhere" ,
386+ )
387+ return "created-1"
388+
389+ mail .outbox .clear ()
390+ with (
391+ patch ("wlhosted.integrations.tasks.notify_user_change.delay" ),
392+ patch (
393+ "wlhosted.integrations.views.make_unique_username" ,
394+ side_effect = create_concurrent_user ,
395+ ),
396+ ):
397+ response = self .client .post (
398+ reverse ("hosted-api-user-ensure" ),
399+ {
400+ "payload" : dumps (
401+ {
402+ "email" : "created@example.org" ,
403+ "full_name" : "Created User" ,
404+ },
405+ key = TEST_PAYMENT_SECRET ,
406+ salt = USER_ENSURE_SALT ,
407+ )
408+ },
409+ )
410+
411+ self .assertEqual (response .status_code , 200 )
412+ user = User .objects .get (email = "created@example.org" )
413+ payload = loads (
414+ response .json ()["payload" ],
415+ key = TEST_PAYMENT_SECRET ,
416+ salt = USER_ENSURE_RESPONSE_SALT ,
417+ )
418+ self .assertFalse (payload ["created" ])
419+ self .assertEqual (payload ["user" ]["external_id" ], str (user .pk ))
420+ self .assertEqual (user .full_name , "Created Elsewhere" )
421+ self .assertEqual (len (mail .outbox ), 0 )
422+
423+ @override_settings (
424+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
425+ SITE_DOMAIN = "hosted.example.org" ,
426+ )
427+ def test_api_user_ensure_uses_existing_user (self ) -> None :
428+ with patch ("wlhosted.integrations.tasks.notify_user_change.delay" ):
429+ existing = User .objects .create_user (
430+ username = "existing" ,
431+ email = "existing@example.org" ,
432+ password = TESTPASSWORD ,
433+ full_name = "Existing User" ,
434+ )
435+ mail .outbox .clear ()
436+
437+ response = self .client .post (
438+ reverse ("hosted-api-user-ensure" ),
439+ {
440+ "payload" : dumps (
441+ {
442+ "email" : "existing@example.org" ,
443+ "full_name" : "Changed User" ,
444+ },
445+ key = TEST_PAYMENT_SECRET ,
446+ salt = USER_ENSURE_SALT ,
447+ )
448+ },
449+ )
450+
451+ self .assertEqual (response .status_code , 200 )
452+ existing .refresh_from_db ()
453+ self .assertEqual (existing .full_name , "Existing User" )
454+ payload = loads (
455+ response .json ()["payload" ],
456+ key = TEST_PAYMENT_SECRET ,
457+ salt = USER_ENSURE_RESPONSE_SALT ,
458+ )
459+ self .assertFalse (payload ["created" ])
460+ self .assertEqual (payload ["user" ]["external_id" ], str (existing .pk ))
461+ self .assertEqual (len (mail .outbox ), 0 )
462+ self .assertFalse (
463+ AuditLog .objects .filter (user = existing , activity = "external-create" ).exists ()
464+ )
465+
466+ def test_make_unique_username_canonicalizes_email_case (self ) -> None :
467+ self .assertEqual (make_unique_username ("Created@Example.ORG" ), "created" )
468+
469+ @override_settings (PAYMENT_SECRET = TEST_PAYMENT_SECRET )
470+ def test_api_user_ensure_rejects_invalid_payload (self ) -> None :
471+ response = self .client .post (
472+ reverse ("hosted-api-user-ensure" ),
473+ {
474+ "payload" : dumps (
475+ {"email" : "not-an-email" , "full_name" : "Invalid" },
476+ key = TEST_PAYMENT_SECRET ,
477+ salt = USER_ENSURE_SALT ,
478+ )
479+ },
480+ )
481+
482+ self .assertEqual (response .status_code , 400 )
483+
484+ @override_settings (PAYMENT_SECRET = "" )
485+ def test_api_user_ensure_no_payment_secret (self ) -> None :
486+ response = self .client .post (reverse ("hosted-api-user-ensure" ))
295487
296488 self .assertEqual (response .status_code , 400 )
297489
@@ -479,7 +671,7 @@ def run_recurring(self, *, add_project: bool = True, add_user: bool = True) -> N
479671 bill = Billing .objects .get ()
480672 project = Project .objects .create (name = "Project" , slug = "project" )
481673 if add_project :
482- bill .projects . add (project )
674+ bill .add_project (project )
483675 if add_user :
484676 project .add_user (self .user )
485677 # Invoke recurring payment
0 commit comments