package users_test import ( "context" "errors" "testing" "github.com/google/uuid" "backend/internal/modules/users" "backend/internal/platform/security" ) // fakeRepository is an in-memory users.Repository used to unit-test // users.Service without a real database. type fakeRepository struct { byID map[uuid.UUID]*users.User byEmail map[string]uuid.UUID } func newFakeRepository() *fakeRepository { return &fakeRepository{ byID: make(map[uuid.UUID]*users.User), byEmail: make(map[string]uuid.UUID), } } func (r *fakeRepository) Create(_ context.Context, u *users.User) error { if _, exists := r.byEmail[u.Email]; exists { return users.ErrEmailTaken } cp := *u r.byID[u.ID] = &cp r.byEmail[u.Email] = u.ID return nil } func (r *fakeRepository) FindByEmail(_ context.Context, email string) (*users.User, error) { id, ok := r.byEmail[email] if !ok { return nil, users.ErrNotFound } cp := *r.byID[id] return &cp, nil } func (r *fakeRepository) FindByID(_ context.Context, id uuid.UUID) (*users.User, error) { u, ok := r.byID[id] if !ok { return nil, users.ErrNotFound } cp := *u return &cp, nil } func (r *fakeRepository) List(_ context.Context) ([]*users.User, error) { list := make([]*users.User, 0, len(r.byID)) for _, u := range r.byID { cp := *u list = append(list, &cp) } return list, nil } func (r *fakeRepository) Update(_ context.Context, u *users.User) error { existing, ok := r.byID[u.ID] if !ok { return users.ErrNotFound } if existing.Email != u.Email { if _, taken := r.byEmail[u.Email]; taken { return users.ErrEmailTaken } delete(r.byEmail, existing.Email) r.byEmail[u.Email] = u.ID } cp := *u r.byID[u.ID] = &cp return nil } func (r *fakeRepository) Delete(_ context.Context, id uuid.UUID) error { u, ok := r.byID[id] if !ok { return users.ErrNotFound } delete(r.byEmail, u.Email) delete(r.byID, id) return nil } func TestService_Create_HashesPasswordAndPersists(t *testing.T) { svc := users.NewService(newFakeRepository()) ctx := context.Background() user, err := svc.Create(ctx, "admin@example.com", "super-strong-password", users.RoleAdmin) if err != nil { t.Fatalf("Create() error = %v", err) } if user.PasswordHash == "super-strong-password" { t.Fatal("Create() stored the plaintext password instead of a hash") } ok, err := security.VerifyPassword(user.PasswordHash, "super-strong-password") if err != nil || !ok { t.Fatalf("stored password hash does not verify against the original password (ok=%v, err=%v)", ok, err) } } func TestService_Create_RejectsInvalidRole(t *testing.T) { svc := users.NewService(newFakeRepository()) if _, err := svc.Create(context.Background(), "a@example.com", "super-strong-password", "superadmin"); err == nil { t.Fatal("Create() error = nil, want error for an invalid role") } } func TestService_Create_DuplicateEmailRejected(t *testing.T) { svc := users.NewService(newFakeRepository()) ctx := context.Background() if _, err := svc.Create(ctx, "dup@example.com", "super-strong-password", users.RoleAdmin); err != nil { t.Fatalf("first Create() error = %v", err) } _, err := svc.Create(ctx, "dup@example.com", "another-strong-password", users.RoleAdmin) if !errors.Is(err, users.ErrEmailTaken) { t.Fatalf("second Create() error = %v, want ErrEmailTaken", err) } } func TestService_SetPassword_ChangesHash(t *testing.T) { svc := users.NewService(newFakeRepository()) ctx := context.Background() user, err := svc.Create(ctx, "user@example.com", "first-strong-password", users.RoleAdmin) if err != nil { t.Fatalf("Create() error = %v", err) } oldHash := user.PasswordHash if err := svc.SetPassword(ctx, user.ID, "second-strong-password"); err != nil { t.Fatalf("SetPassword() error = %v", err) } updated, err := svc.FindByID(ctx, user.ID) if err != nil { t.Fatalf("FindByID() error = %v", err) } if updated.PasswordHash == oldHash { t.Fatal("SetPassword() did not change the stored hash") } ok, _ := security.VerifyPassword(updated.PasswordHash, "second-strong-password") if !ok { t.Fatal("new password does not verify against the updated hash") } ok, _ = security.VerifyPassword(updated.PasswordHash, "first-strong-password") if ok { t.Fatal("old password still verifies after SetPassword()") } } func TestService_Delete_RemovesUser(t *testing.T) { svc := users.NewService(newFakeRepository()) ctx := context.Background() user, err := svc.Create(ctx, "todelete@example.com", "super-strong-password", users.RoleAdmin) if err != nil { t.Fatalf("Create() error = %v", err) } if err := svc.Delete(ctx, user.ID); err != nil { t.Fatalf("Delete() error = %v", err) } if _, err := svc.FindByID(ctx, user.ID); !errors.Is(err, users.ErrNotFound) { t.Fatalf("FindByID() after delete error = %v, want ErrNotFound", err) } } func TestService_Delete_UnknownUserReturnsNotFound(t *testing.T) { svc := users.NewService(newFakeRepository()) if err := svc.Delete(context.Background(), uuid.New()); !errors.Is(err, users.ErrNotFound) { t.Fatalf("Delete() error = %v, want ErrNotFound", err) } }