@@ -177,16 +177,22 @@ async def test_authentication_and_role_mapping(
177177 status , access_token , refresh_token = _ldap_login (_app , admin .username , admin .password )
178178 _verify_ldap_login_success (status , access_token , refresh_token )
179179 admin_user = _verify_user_created (_app , admin )
180- assert admin_user .role == UserRoleInput .ADMIN
180+
181+ # Member user (in members group)
182+ member = _create_test_user (_ldap_server , suffix , "member" , UserRoleInput .MEMBER )
183+ status , access_token , refresh_token = _ldap_login (_app , member .username , member .password )
184+ _verify_ldap_login_success (status , access_token , refresh_token )
185+ member_user = _verify_user_created (_app , member )
181186
182187 # Viewer user (no groups → wildcard)
183188 viewer = _create_test_user (_ldap_server , suffix , "viewer" , UserRoleInput .VIEWER , groups = [])
184189 status , access_token , refresh_token = _ldap_login (_app , viewer .username , viewer .password )
185190 _verify_ldap_login_success (status , access_token , refresh_token )
186191 viewer_user = _verify_user_created (_app , viewer )
187- assert viewer_user .role == UserRoleInput .VIEWER
188192
189- _delete_users (_app , _app .admin_secret , users = [admin_user .gid , viewer_user .gid ])
193+ _delete_users (
194+ _app , _app .admin_secret , users = [admin_user .gid , member_user .gid , viewer_user .gid ]
195+ )
190196
191197 async def test_invalid_credentials_rejected (
192198 self , _app : _AppInfo , _ldap_server : _LDAPServer
@@ -243,7 +249,17 @@ async def test_role_syncs_on_subsequent_login(
243249
244250 async def test_injection_prevention (self , _app : _AppInfo , _ldap_server : _LDAPServer ) -> None :
245251 """Test LDAP injection attempts are rejected."""
246- for payload in ["*" , "admin*" , "*(objectClass=*)" , "admin)(|(objectClass=*" ]:
252+ payloads = [
253+ "*" , # Wildcard
254+ "admin*" , # Wildcard suffix
255+ "*(objectClass=*)" , # Filter injection
256+ "admin)(|(objectClass=*" , # Filter escape
257+ "admin\x00 injected" , # Null byte injection
258+ "admin\n injected" , # Newline injection
259+ "admin\r \n injected" , # CRLF injection
260+ ")(cn=*" , # DN injection
261+ ]
262+ for payload in payloads :
247263 assert _ldap_login (_app , payload , _DEFAULT_PASSWORD )[0 ] == 401
248264
249265 async def test_unicode_credentials (self , _app : _AppInfo , _ldap_server : _LDAPServer ) -> None :
@@ -263,6 +279,28 @@ async def test_unicode_credentials(self, _app: _AppInfo, _ldap_server: _LDAPServ
263279 assert user is not None
264280 _delete_users (_app , _app .admin_secret , users = [user .gid ])
265281
282+ async def test_special_characters_in_password (
283+ self , _app : _AppInfo , _ldap_server : _LDAPServer
284+ ) -> None :
285+ """Test login with special characters in password (quotes, backslashes, etc.)."""
286+ suffix = token_hex (4 )
287+ email = f"special_{ suffix } @example.com"
288+ special_password = r'p@ss"word\with\'special<chars>&more!'
289+ _ldap_server .add_user (
290+ username = f"special_{ suffix } " ,
291+ password = special_password ,
292+ email = email ,
293+ display_name = "Special User" ,
294+ groups = [_MEMBER_GROUP ],
295+ )
296+ status , access_token , refresh_token = _ldap_login (
297+ _app , f"special_{ suffix } " , special_password
298+ )
299+ _verify_ldap_login_success (status , access_token , refresh_token )
300+ user = _get_user_by_email (_app , email )
301+ assert user is not None
302+ _delete_users (_app , _app .admin_secret , users = [user .gid ])
303+
266304 async def test_missing_email_rejected (self , _app : _AppInfo , _ldap_server : _LDAPServer ) -> None :
267305 """Test login fails when LDAP user has no email."""
268306 suffix = token_hex (4 )
@@ -298,7 +336,11 @@ async def test_missing_display_name_uses_fallback(
298336 async def test_multiple_groups_uses_first_match (
299337 self , _app : _AppInfo , _ldap_server : _LDAPServer
300338 ) -> None :
301- """Test user in multiple groups gets role from first matching mapping."""
339+ """Test user in multiple groups gets role from first matching mapping.
340+
341+ group_role_mappings is evaluated in order: ADMIN, MEMBER, VIEWER (wildcard).
342+ User is in both MEMBER and ADMIN groups, but ADMIN mapping is checked first.
343+ """
302344 suffix = token_hex (4 )
303345 email = f"multi_{ suffix } @example.com"
304346 _ldap_server .add_user (
@@ -312,7 +354,7 @@ async def test_multiple_groups_uses_first_match(
312354 assert status == 204
313355 user = _get_user_by_email (_app , email )
314356 assert user is not None
315- assert user .role == UserRoleInput .ADMIN # ADMIN mapping comes first in config
357+ assert user .role == UserRoleInput .ADMIN # ADMIN mapping evaluated before MEMBER
316358 _delete_users (_app , _app .admin_secret , users = [user .gid ])
317359
318360 async def test_group_dn_case_insensitive (
@@ -417,6 +459,8 @@ def test_email_change_preserves_identity_with_unique_id(
417459 },
418460 )
419461 assert response .status_code == 200
462+ response_json = response .json ()
463+ assert not response_json .get ("errors" )
420464
421465 # First login (migrates pre-provisioned user to unique_id)
422466 _ldap_server .add_user (
@@ -449,6 +493,8 @@ def test_email_change_preserves_identity_with_unique_id(
449493 user_v2 = _get_user_by_email (app , email_v2 )
450494 assert user_v2 is not None
451495 assert user_v2 .gid == user_v1 .gid , "Same user when unique_id is configured"
496+ assert user_v2 .role == UserRoleInput .ADMIN
497+ assert user_v2 .username == "Pre-Provisioned"
452498
453499 # Old email no longer exists
454500 assert _get_user_by_email (app , email_v1 ) is None
@@ -499,7 +545,9 @@ async def test_ldap_user_graphql_auth_method(
499545 )
500546
501547 assert graphql_response .status_code == 200
502- data = graphql_response .json ()
548+ graphql_data = graphql_response .json ()
549+ assert not graphql_data .get ("errors" ), graphql_data .get ("errors" )
550+ data = graphql_data
503551
504552 # Find our LDAP user in the response
505553 users = data ["data" ]["users" ]["edges" ]
@@ -659,7 +707,9 @@ def test_ldap_allow_sign_up_false_with_email_lookup(
659707 },
660708 )
661709 assert create_response .status_code == 200
662- user_data = create_response .json ()["data" ]["createUser" ]["user" ]
710+ create_json = create_response .json ()
711+ assert not create_json .get ("errors" ), create_json .get ("errors" )
712+ user_data = create_json ["data" ]["createUser" ]["user" ]
663713 assert user_data ["authMethod" ] == "LDAP"
664714 created_user_gid = user_data ["id" ]
665715
@@ -684,6 +734,7 @@ def test_ldap_allow_sign_up_false_with_email_lookup(
684734 assert updated_user is not None
685735 # Username stays stable from admin creation (prevents collisions on displayName changes)
686736 assert updated_user .profile .username == "John Doe"
737+ assert updated_user .role == UserRoleInput .ADMIN
687738
688739 # Step 5: Verify subsequent logins work
689740 status , access_token , refresh_token = _ldap_login (
0 commit comments