@@ -31,7 +31,7 @@ impl From<LocalActor> for Actor {
3131 }
3232}
3333
34- #[ derive( sqlx:: Decode , sqlx:: Encode , sqlx:: FromRow ) ]
34+ #[ derive( Debug , sqlx:: Decode , sqlx:: Encode , sqlx:: FromRow ) ]
3535/// Actors from this home server.
3636pub struct LocalActor {
3737 /// The unique actor identifer. Does not change, even if the `local_name`
@@ -161,6 +161,8 @@ pub struct Invite {
161161
162162#[ cfg( test) ]
163163mod tests {
164+ use sqlx:: { Pool , Postgres } ;
165+
164166 use super :: * ;
165167
166168 #[ test]
@@ -177,4 +179,200 @@ mod tests {
177179 assert_eq ! ( algo_id. common_name, Some ( "SHA256withRSA" . to_string( ) ) ) ;
178180 assert_eq ! ( algo_id. parameters, Some ( "null" . to_string( ) ) ) ;
179181 }
182+
183+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
184+ async fn test_by_local_name_finds_existing_user ( pool : Pool < Postgres > ) {
185+ let db = Database { pool } ;
186+
187+ let result = LocalActor :: by_local_name ( & db, "alice" ) . await . unwrap ( ) ;
188+ assert ! ( result. is_some( ) ) ;
189+
190+ let actor = result. unwrap ( ) ;
191+ assert_eq ! ( actor. local_name, "alice" ) ;
192+ assert_eq ! (
193+ actor. unique_actor_identifier. to_string( ) ,
194+ "00000000-0000-0000-0000-000000000001"
195+ ) ;
196+ assert ! ( !actor. is_deactivated) ;
197+ }
198+
199+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
200+ async fn test_by_local_name_finds_deactivated_user ( pool : Pool < Postgres > ) {
201+ let db = Database { pool } ;
202+
203+ let result = LocalActor :: by_local_name ( & db, "deactivated_user" ) . await . unwrap ( ) ;
204+ assert ! ( result. is_some( ) ) ;
205+
206+ let actor = result. unwrap ( ) ;
207+ assert_eq ! ( actor. local_name, "deactivated_user" ) ;
208+ assert_eq ! (
209+ actor. unique_actor_identifier. to_string( ) ,
210+ "00000000-0000-0000-0000-000000000004"
211+ ) ;
212+ assert ! ( actor. is_deactivated) ;
213+ }
214+
215+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
216+ async fn test_by_local_name_finds_user_with_special_characters ( pool : Pool < Postgres > ) {
217+ let db = Database { pool } ;
218+
219+ let result = LocalActor :: by_local_name ( & db, "user_with_underscores" ) . await . unwrap ( ) ;
220+ assert ! ( result. is_some( ) ) ;
221+
222+ let actor = result. unwrap ( ) ;
223+ assert_eq ! ( actor. local_name, "user_with_underscores" ) ;
224+ assert_eq ! (
225+ actor. unique_actor_identifier. to_string( ) ,
226+ "00000000-0000-0000-0000-000000000005"
227+ ) ;
228+ assert ! ( !actor. is_deactivated) ;
229+ }
230+
231+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
232+ async fn test_by_local_name_returns_none_for_nonexistent_user ( pool : Pool < Postgres > ) {
233+ let db = Database { pool } ;
234+
235+ let result = LocalActor :: by_local_name ( & db, "nonexistent_user" ) . await . unwrap ( ) ;
236+ assert ! ( result. is_none( ) ) ;
237+ }
238+
239+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
240+ async fn test_by_local_name_returns_none_for_empty_string ( pool : Pool < Postgres > ) {
241+ let db = Database { pool } ;
242+
243+ let result = LocalActor :: by_local_name ( & db, "" ) . await . unwrap ( ) ;
244+ assert ! ( result. is_none( ) ) ;
245+ }
246+
247+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
248+ async fn test_by_local_name_is_case_sensitive ( pool : Pool < Postgres > ) {
249+ let db = Database { pool } ;
250+
251+ // Should find exact match
252+ let result_exact = LocalActor :: by_local_name ( & db, "alice" ) . await . unwrap ( ) ;
253+ assert ! ( result_exact. is_some( ) ) ;
254+
255+ // Should not find case-different match
256+ let result_upper = LocalActor :: by_local_name ( & db, "ALICE" ) . await . unwrap ( ) ;
257+ assert ! ( result_upper. is_none( ) ) ;
258+
259+ let result_mixed = LocalActor :: by_local_name ( & db, "Alice" ) . await . unwrap ( ) ;
260+ assert ! ( result_mixed. is_none( ) ) ;
261+ }
262+
263+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
264+ async fn test_create_new_user_success ( pool : Pool < Postgres > ) {
265+ let db = Database { pool } ;
266+
267+ let result = LocalActor :: create ( & db, "new_user" ) . await ;
268+ assert ! ( result. is_ok( ) ) ;
269+
270+ let actor = result. unwrap ( ) ;
271+ assert_eq ! ( actor. local_name, "new_user" ) ;
272+ assert ! ( !actor. is_deactivated) ;
273+ assert ! ( actor. unique_actor_identifier != sqlx:: types:: Uuid :: nil( ) ) ;
274+
275+ // Verify the user was actually created in the database
276+ let found = LocalActor :: by_local_name ( & db, "new_user" ) . await . unwrap ( ) ;
277+ assert ! ( found. is_some( ) ) ;
278+ let found_actor = found. unwrap ( ) ;
279+ assert_eq ! ( found_actor. unique_actor_identifier, actor. unique_actor_identifier) ;
280+ }
281+
282+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
283+ async fn test_create_duplicate_user_returns_error ( pool : Pool < Postgres > ) {
284+ let db = Database { pool } ;
285+
286+ let result = LocalActor :: create ( & db, "alice" ) . await ;
287+ assert ! ( result. is_err( ) ) ;
288+
289+ match result. unwrap_err ( ) {
290+ SonataApiError :: Error ( error) => {
291+ assert_eq ! ( error. code, Errcode :: Duplicate ) ;
292+ assert ! ( error. context. is_some( ) ) ;
293+ let context = error. context . unwrap ( ) ;
294+ assert_eq ! ( context. field_name, "local_name" ) ;
295+ assert_eq ! ( context. found, "alice" ) ;
296+ }
297+ _ => panic ! ( "Expected SonataApiError::Error with Duplicate errcode" ) ,
298+ }
299+ }
300+
301+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
302+ async fn test_create_duplicate_deactivated_user_returns_error ( pool : Pool < Postgres > ) {
303+ let db = Database { pool } ;
304+
305+ let result = LocalActor :: create ( & db, "deactivated_user" ) . await ;
306+ assert ! ( result. is_err( ) ) ;
307+
308+ match result. unwrap_err ( ) {
309+ SonataApiError :: Error ( error) => {
310+ assert_eq ! ( error. code, Errcode :: Duplicate ) ;
311+ assert ! ( error. context. is_some( ) ) ;
312+ let context = error. context . unwrap ( ) ;
313+ assert_eq ! ( context. field_name, "local_name" ) ;
314+ assert_eq ! ( context. found, "deactivated_user" ) ;
315+ }
316+ _ => panic ! ( "Expected SonataApiError::Error with Duplicate errcode" ) ,
317+ }
318+ }
319+
320+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
321+ async fn test_create_user_with_special_characters ( pool : Pool < Postgres > ) {
322+ let db = Database { pool } ;
323+
324+ let result = LocalActor :: create ( & db, "user.with-special_chars" ) . await ;
325+ assert ! ( result. is_ok( ) ) ;
326+
327+ let actor = result. unwrap ( ) ;
328+ assert_eq ! ( actor. local_name, "user.with-special_chars" ) ;
329+ assert ! ( !actor. is_deactivated) ;
330+
331+ let found = LocalActor :: by_local_name ( & db, "user.with-special_chars" ) . await . unwrap ( ) ;
332+ assert ! ( found. is_some( ) ) ;
333+ }
334+
335+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
336+ async fn test_create_user_with_empty_name ( pool : Pool < Postgres > ) {
337+ let db = Database { pool } ;
338+
339+ let result = LocalActor :: create ( & db, "" ) . await ;
340+ assert ! ( result. is_ok( ) ) ;
341+
342+ let actor = result. unwrap ( ) ;
343+ assert_eq ! ( actor. local_name, "" ) ;
344+ assert ! ( !actor. is_deactivated) ;
345+
346+ let found = LocalActor :: by_local_name ( & db, "" ) . await . unwrap ( ) ;
347+ assert ! ( found. is_some( ) ) ;
348+ }
349+
350+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
351+ async fn test_create_multiple_users_have_different_uuids ( pool : Pool < Postgres > ) {
352+ let db = Database { pool } ;
353+
354+ let user1 = LocalActor :: create ( & db, "user1" ) . await . unwrap ( ) ;
355+ let user2 = LocalActor :: create ( & db, "user2" ) . await . unwrap ( ) ;
356+ let user3 = LocalActor :: create ( & db, "user3" ) . await . unwrap ( ) ;
357+
358+ assert_ne ! ( user1. unique_actor_identifier, user2. unique_actor_identifier) ;
359+ assert_ne ! ( user1. unique_actor_identifier, user3. unique_actor_identifier) ;
360+ assert_ne ! ( user2. unique_actor_identifier, user3. unique_actor_identifier) ;
361+
362+ assert_ne ! ( user1. local_name, user2. local_name) ;
363+ assert_ne ! ( user1. local_name, user3. local_name) ;
364+ assert_ne ! ( user2. local_name, user3. local_name) ;
365+ }
366+
367+ #[ sqlx:: test( fixtures( "../../../fixtures/local_actor_tests.sql" ) ) ]
368+ async fn test_create_user_sets_joined_timestamp ( pool : Pool < Postgres > ) {
369+ let db = Database { pool } ;
370+
371+ let before_create = chrono:: Utc :: now ( ) . naive_utc ( ) ;
372+ let actor = LocalActor :: create ( & db, "timestamped_user" ) . await . unwrap ( ) ;
373+ let after_create = chrono:: Utc :: now ( ) . naive_utc ( ) ;
374+
375+ assert ! ( actor. joined_at_timestamp >= before_create) ;
376+ assert ! ( actor. joined_at_timestamp <= after_create) ;
377+ }
180378}
0 commit comments