@@ -120,6 +120,60 @@ def test_handle_dovecot_protocol_iterate(gencreds, example_config):
120120 assert not lines [2 ]
121121
122122
123+ def test_invalid_localpart_characters (make_config ):
124+ """Test that is_allowed_to_create rejects localparts with invalid characters."""
125+ config = make_config ("chat.example.org" , {"username_min_length" : "3" })
126+ password = "zequ0Aimuchoodaechik"
127+ domain = config .mail_domain
128+
129+ # valid localparts
130+ assert is_allowed_to_create (config , f"abc123@{ domain } " , password )
131+ assert is_allowed_to_create (config , f"a.b-c_d@{ domain } " , password )
132+
133+ # uppercase rejected
134+ assert not is_allowed_to_create (config , f"Abc123@{ domain } " , password )
135+ assert not is_allowed_to_create (config , f"ABCDEFG@{ domain } " , password )
136+
137+ # spaces and special chars rejected
138+ assert not is_allowed_to_create (config , f"a b cde@{ domain } " , password )
139+ assert not is_allowed_to_create (config , f"abc+def@{ domain } " , password )
140+ assert not is_allowed_to_create (config , f"abc!def@{ domain } " , password )
141+ assert not is_allowed_to_create (config , f"ab@cdef@{ domain } " , password )
142+ assert not is_allowed_to_create (config , f"abc/def@{ domain } " , password )
143+ assert not is_allowed_to_create (config , f"abc\\ def@{ domain } " , password )
144+
145+
146+ def test_concurrent_creation_same_account (dictproxy ):
147+ """Test that concurrent creation of the same account doesn't corrupt password."""
148+ addr = "racetest1@chat.example.org"
149+ password = "zequ0Aimuchoodaechik"
150+ num_threads = 10
151+ results = queue .Queue ()
152+
153+ def create ():
154+ try :
155+ res = dictproxy .lookup_passdb (addr , password )
156+ results .put (("ok" , res ))
157+ except Exception :
158+ results .put (("err" , traceback .format_exc ()))
159+
160+ threads = [threading .Thread (target = create , daemon = True ) for _ in range (num_threads )]
161+ for t in threads :
162+ t .start ()
163+ for t in threads :
164+ t .join (timeout = 10 )
165+
166+ passwords_seen = set ()
167+ for _ in range (num_threads ):
168+ status , res = results .get ()
169+ if status == "err" :
170+ pytest .fail (f"concurrent creation failed\n { res } " )
171+ passwords_seen .add (res ["password" ])
172+
173+ # all threads must see the same password hash
174+ assert len (passwords_seen ) == 1
175+
176+
123177def test_50_concurrent_lookups_different_accounts (gencreds , dictproxy ):
124178 num_threads = 50
125179 req_per_thread = 5
0 commit comments