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+ )
4754from wlhosted .payments .backends import get_backend
4855from wlhosted .payments .models import Customer , Payment
4956
@@ -197,7 +204,7 @@ def test_api_users(self) -> None:
197204 "payload" : dumps (
198205 {"since" : "" },
199206 key = TEST_PAYMENT_SECRET ,
200- salt = "weblate.user-sync" ,
207+ salt = USER_SYNC_SALT ,
201208 )
202209 },
203210 )
@@ -207,7 +214,7 @@ def test_api_users(self) -> None:
207214 payload = loads (
208215 response .json ()["payload" ],
209216 key = TEST_PAYMENT_SECRET ,
210- salt = "weblate.user-sync-response" ,
217+ salt = USER_SYNC_RESPONSE_SALT ,
211218 )
212219 self .assertIn (
213220 str (self .user .pk ), {user ["external_id" ] for user in payload ["users" ]}
@@ -221,7 +228,7 @@ def test_api_users_requires_post(self) -> None:
221228 "payload" : dumps (
222229 {"since" : "" },
223230 key = TEST_PAYMENT_SECRET ,
224- salt = "weblate.user-sync" ,
231+ salt = USER_SYNC_SALT ,
225232 )
226233 },
227234 )
@@ -252,7 +259,7 @@ def test_api_users_incremental(self) -> None:
252259 "payload" : dumps (
253260 {"since" : since .isoformat ()},
254261 key = TEST_PAYMENT_SECRET ,
255- salt = "weblate.user-sync" ,
262+ salt = USER_SYNC_SALT ,
256263 )
257264 },
258265 )
@@ -261,7 +268,7 @@ def test_api_users_incremental(self) -> None:
261268 payload = loads (
262269 response .json ()["payload" ],
263270 key = TEST_PAYMENT_SECRET ,
264- salt = "weblate.user-sync-response" ,
271+ salt = USER_SYNC_RESPONSE_SALT ,
265272 )
266273 self .assertEqual (
267274 [user ["external_id" ] for user in payload ["users" ]],
@@ -279,7 +286,7 @@ def test_api_users_invalid_cursor(self) -> None:
279286 "payload" : dumps (
280287 {"since" : since },
281288 key = TEST_PAYMENT_SECRET ,
282- salt = "weblate.user-sync" ,
289+ salt = USER_SYNC_SALT ,
283290 )
284291 },
285292 )
@@ -290,11 +297,174 @@ def test_api_users_invalid_cursor(self) -> None:
290297 def test_api_users_no_payment_secret (self ) -> None :
291298 response = self .client .post (
292299 reverse ("hosted-api-users" ),
293- {"payload" : dumps ({"since" : "" }, key = "" , salt = "weblate.user-sync" )},
300+ {"payload" : dumps ({"since" : "" }, key = "" , salt = USER_SYNC_SALT )},
294301 )
295302
296303 self .assertEqual (response .status_code , 400 )
297304
305+ @override_settings (
306+ ENABLE_HTTPS = True ,
307+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
308+ SITE_DOMAIN = "hosted.example.org" ,
309+ )
310+ def test_api_user_ensure_creates_user (self ) -> None :
311+ mail .outbox .clear ()
312+ with patch ("wlhosted.integrations.tasks.notify_user_change.delay" ):
313+ response = self .client .post (
314+ reverse ("hosted-api-user-ensure" ),
315+ {
316+ "payload" : dumps (
317+ {
318+ "email" : "created@example.org" ,
319+ "full_name" : "Created User" ,
320+ },
321+ key = TEST_PAYMENT_SECRET ,
322+ salt = USER_ENSURE_SALT ,
323+ )
324+ },
325+ )
326+
327+ self .assertEqual (response .status_code , 200 )
328+ user = User .objects .get (email = "created@example.org" )
329+ self .assertEqual (user .username , "created" )
330+ self .assertEqual (user .full_name , "Created User" )
331+ self .assertFalse (user .has_usable_password ())
332+ payload = loads (
333+ response .json ()["payload" ],
334+ key = TEST_PAYMENT_SECRET ,
335+ salt = USER_ENSURE_RESPONSE_SALT ,
336+ )
337+ self .assertTrue (payload ["created" ])
338+ self .assertEqual (payload ["user" ]["external_id" ], str (user .pk ))
339+
340+ audit = AuditLog .objects .get (user = user , activity = "external-create" )
341+ self .assertIn ("created externally from weblate.org" , audit .get_message ())
342+ self .assertEqual (len (mail .outbox ), 1 )
343+ self .assertIn (
344+ "weblate.org created this Hosted Weblate sign-in account" ,
345+ mail .outbox [0 ].body ,
346+ )
347+ self .assertIn ("purchased Weblate services" , mail .outbox [0 ].body )
348+ self .assertIn ("Set account password" , mail .outbox [0 ].body )
349+ self .assertIn ("verification_code=" , mail .outbox [0 ].body )
350+ self .assertIn (
351+ f"https://hosted.example.org{ reverse ('social:complete' , args = ('email' ,))} " ,
352+ mail .outbox [0 ].body ,
353+ )
354+
355+ @override_settings (
356+ ENABLE_HTTPS = True ,
357+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
358+ SITE_DOMAIN = "hosted.example.org" ,
359+ )
360+ def test_api_user_ensure_uses_concurrently_created_user (self ) -> None :
361+ def create_concurrent_user (email : str ) -> str :
362+ User .objects .create_user (
363+ username = "created" ,
364+ email = email ,
365+ password = TESTPASSWORD ,
366+ full_name = "Created Elsewhere" ,
367+ )
368+ return "created-1"
369+
370+ mail .outbox .clear ()
371+ with (
372+ patch ("wlhosted.integrations.tasks.notify_user_change.delay" ),
373+ patch (
374+ "wlhosted.integrations.views.make_unique_username" ,
375+ side_effect = create_concurrent_user ,
376+ ),
377+ ):
378+ response = self .client .post (
379+ reverse ("hosted-api-user-ensure" ),
380+ {
381+ "payload" : dumps (
382+ {
383+ "email" : "created@example.org" ,
384+ "full_name" : "Created User" ,
385+ },
386+ key = TEST_PAYMENT_SECRET ,
387+ salt = USER_ENSURE_SALT ,
388+ )
389+ },
390+ )
391+
392+ self .assertEqual (response .status_code , 200 )
393+ user = User .objects .get (email = "created@example.org" )
394+ payload = loads (
395+ response .json ()["payload" ],
396+ key = TEST_PAYMENT_SECRET ,
397+ salt = USER_ENSURE_RESPONSE_SALT ,
398+ )
399+ self .assertFalse (payload ["created" ])
400+ self .assertEqual (payload ["user" ]["external_id" ], str (user .pk ))
401+ self .assertEqual (user .full_name , "Created Elsewhere" )
402+ self .assertEqual (len (mail .outbox ), 0 )
403+
404+ @override_settings (
405+ PAYMENT_SECRET = TEST_PAYMENT_SECRET ,
406+ SITE_DOMAIN = "hosted.example.org" ,
407+ )
408+ def test_api_user_ensure_uses_existing_user (self ) -> None :
409+ with patch ("wlhosted.integrations.tasks.notify_user_change.delay" ):
410+ existing = User .objects .create_user (
411+ username = "existing" ,
412+ email = "existing@example.org" ,
413+ password = TESTPASSWORD ,
414+ full_name = "Existing User" ,
415+ )
416+ mail .outbox .clear ()
417+
418+ response = self .client .post (
419+ reverse ("hosted-api-user-ensure" ),
420+ {
421+ "payload" : dumps (
422+ {
423+ "email" : "existing@example.org" ,
424+ "full_name" : "Changed User" ,
425+ },
426+ key = TEST_PAYMENT_SECRET ,
427+ salt = USER_ENSURE_SALT ,
428+ )
429+ },
430+ )
431+
432+ self .assertEqual (response .status_code , 200 )
433+ existing .refresh_from_db ()
434+ self .assertEqual (existing .full_name , "Existing User" )
435+ payload = loads (
436+ response .json ()["payload" ],
437+ key = TEST_PAYMENT_SECRET ,
438+ salt = USER_ENSURE_RESPONSE_SALT ,
439+ )
440+ self .assertFalse (payload ["created" ])
441+ self .assertEqual (payload ["user" ]["external_id" ], str (existing .pk ))
442+ self .assertEqual (len (mail .outbox ), 0 )
443+ self .assertFalse (
444+ AuditLog .objects .filter (user = existing , activity = "external-create" ).exists ()
445+ )
446+
447+ @override_settings (PAYMENT_SECRET = TEST_PAYMENT_SECRET )
448+ def test_api_user_ensure_rejects_invalid_payload (self ) -> None :
449+ response = self .client .post (
450+ reverse ("hosted-api-user-ensure" ),
451+ {
452+ "payload" : dumps (
453+ {"email" : "not-an-email" , "full_name" : "Invalid" },
454+ key = TEST_PAYMENT_SECRET ,
455+ salt = USER_ENSURE_SALT ,
456+ )
457+ },
458+ )
459+
460+ self .assertEqual (response .status_code , 400 )
461+
462+ @override_settings (PAYMENT_SECRET = "" )
463+ def test_api_user_ensure_no_payment_secret (self ) -> None :
464+ response = self .client .post (reverse ("hosted-api-user-ensure" ))
465+
466+ self .assertEqual (response .status_code , 400 )
467+
298468 def test_pending_payments (self ) -> None :
299469 self .test_create ()
300470 Payment .objects .all ().update (state = Payment .ACCEPTED )
0 commit comments