@@ -338,3 +338,157 @@ def test_github_oauth_missing_code(self, webtest):
338338 )
339339 assert error_message is not None
340340 assert "No authorization code received from GitHub" in error_message .text
341+
342+ def test_connect_gitlab_account (self , webtest ):
343+ """A user can connect a GitLab account via OAuth."""
344+ user = UserFactory .create (
345+ with_verified_primary_email = True ,
346+ with_terms_of_service_agreement = True ,
347+ clear_pwd = "password" ,
348+ )
349+
350+ self ._login_user (webtest , user )
351+
352+ # Click "Connect GitLab" button (initiates OAuth flow)
353+ connect_response = webtest .get (
354+ "/manage/account/associations/gitlab/connect" ,
355+ status = HTTPStatus .SEE_OTHER ,
356+ )
357+
358+ # Follow the redirect to the callback URL
359+ parsed_url = parse_url (connect_response .location )
360+ callback_response = webtest .get (
361+ parsed_url .path ,
362+ params = parsed_url .query ,
363+ status = HTTPStatus .SEE_OTHER ,
364+ )
365+
366+ # Follow redirect back to account page
367+ account_page = callback_response .follow (status = HTTPStatus .OK )
368+
369+ # Verify association was created
370+ assert "Account associations" in account_page .text
371+ assert "mockuser_" in account_page .text
372+
373+ def test_connect_gitlab_account_invalid_state (self , webtest ):
374+ """GitLab OAuth flow rejects requests with invalid state tokens."""
375+ user = UserFactory .create (
376+ with_verified_primary_email = True ,
377+ with_terms_of_service_agreement = True ,
378+ clear_pwd = "password" ,
379+ )
380+
381+ self ._login_user (webtest , user )
382+
383+ # Try to access callback directly with invalid state
384+ callback_response = webtest .get (
385+ "/manage/account/associations/gitlab/callback" ,
386+ params = {"code" : "test" , "state" : "invalid" },
387+ status = HTTPStatus .SEE_OTHER ,
388+ )
389+
390+ # Should redirect to account page with error
391+ account_page = callback_response .follow (status = HTTPStatus .OK )
392+ # Verify no association was created
393+ assert "You have not connected" in account_page .text
394+
395+ def test_gitlab_oauth_error_response (self , webtest ):
396+ """GitLab OAuth flow handles error responses from GitLab."""
397+ user = UserFactory .create (
398+ with_verified_primary_email = True ,
399+ with_terms_of_service_agreement = True ,
400+ clear_pwd = "password" ,
401+ )
402+
403+ self ._login_user (webtest , user )
404+
405+ # Start OAuth flow to get a valid state token
406+ connect_response = webtest .get (
407+ "/manage/account/associations/gitlab/connect" ,
408+ status = HTTPStatus .SEE_OTHER ,
409+ )
410+ parsed_url = parse_url (connect_response .location )
411+ state = parsed_url .query .split ("state=" )[1 ].split ("&" )[0 ]
412+
413+ # Simulate OAuth error response with valid state
414+ callback_response = webtest .get (
415+ "/manage/account/associations/gitlab/callback" ,
416+ params = {
417+ "state" : state ,
418+ "error" : "access_denied" ,
419+ "error_description" : "User declined" ,
420+ },
421+ status = HTTPStatus .SEE_OTHER ,
422+ )
423+ callback_response .follow (status = HTTPStatus .OK )
424+
425+ # Check flash messages for the error
426+ flash_messages = webtest .get (
427+ "/_includes/unauthed/flash-messages/" , status = HTTPStatus .OK
428+ )
429+ error_message = flash_messages .html .find (
430+ "span" , {"class" : "notification-bar__message" }
431+ )
432+ assert error_message is not None
433+ assert "GitLab OAuth failed" in error_message .text
434+ assert "User declined" in error_message .text
435+
436+ def test_gitlab_oauth_missing_code (self , webtest ):
437+ """GitLab OAuth flow handles missing authorization code."""
438+ user = UserFactory .create (
439+ with_verified_primary_email = True ,
440+ with_terms_of_service_agreement = True ,
441+ clear_pwd = "password" ,
442+ )
443+
444+ self ._login_user (webtest , user )
445+
446+ # Start OAuth flow to get a valid state token
447+ connect_response = webtest .get (
448+ "/manage/account/associations/gitlab/connect" ,
449+ status = HTTPStatus .SEE_OTHER ,
450+ )
451+ parsed_url = parse_url (connect_response .location )
452+ state = parsed_url .query .split ("state=" )[1 ].split ("&" )[0 ]
453+
454+ # Call callback with valid state but no code
455+ callback_response = webtest .get (
456+ "/manage/account/associations/gitlab/callback" ,
457+ params = {"state" : state },
458+ status = HTTPStatus .SEE_OTHER ,
459+ )
460+ callback_response .follow (status = HTTPStatus .OK )
461+
462+ # Check flash messages for the error
463+ flash_messages = webtest .get (
464+ "/_includes/unauthed/flash-messages/" ,
465+ status = HTTPStatus .OK ,
466+ )
467+ error_message = flash_messages .html .find (
468+ "span" , {"class" : "notification-bar__message" }
469+ )
470+ assert error_message is not None
471+ assert "No authorization code received from GitLab" in error_message .text
472+
473+ def test_view_account_with_gitlab_association (self , webtest ):
474+ """A user can view a GitLab association on the account page."""
475+ user = UserFactory .create (
476+ with_verified_primary_email = True ,
477+ with_terms_of_service_agreement = True ,
478+ clear_pwd = "password" ,
479+ )
480+ OAuthAccountAssociationFactory .create (
481+ user = user ,
482+ service = "gitlab" ,
483+ external_username = "gitlabuser" ,
484+ )
485+
486+ self ._login_user (webtest , user )
487+
488+ # Visit account settings page
489+ account_page = webtest .get ("/manage/account/" , status = HTTPStatus .OK )
490+
491+ # Verify GitLab association is present
492+ assert "Account associations" in account_page .text
493+ assert "gitlabuser" in account_page .text
494+ assert "gitlab" in account_page .text .lower ()
0 commit comments