package store import ( "errors" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // TestRegisterUser_CreatesUserAndOwnedProject verifies the RegisterUser // transaction: a user and a default project are created together, and the // project belongs to that user. func TestRegisterUser_CreatesUserAndOwnedProject(t *testing.T) { s, ctx := newStore(t) u, p, err := s.RegisterUser(ctx, "alice@example.com", "argon2-hash") if err != nil { t.Fatal(err) } if u.Email != "alice@example.com" || u.PasswordHash != "argon2-hash" { t.Fatalf("unexpected user: %+v", u) } if p.UserID != u.ID { t.Fatalf("expected project to belong to user %s, got %+v", u.ID, p) } owned, err := s.GetProjectOwned(ctx, p.ID, u.ID) if err != nil { t.Fatal(err) } if owned.ID != p.ID { t.Fatalf("expected owned project %s, got %+v", p.ID, owned) } } // TestGetUserByEmail_FindsRegisteredUser verifies email lookup returns the // same user created by RegisterUser. func TestGetUserByEmail_FindsRegisteredUser(t *testing.T) { s, ctx := newStore(t) u, _, err := s.RegisterUser(ctx, "bob@example.com", "argon2-hash") if err != nil { t.Fatal(err) } got, err := s.GetUserByEmail(ctx, "bob@example.com") if err != nil { t.Fatal(err) } if got.ID != u.ID || got.PasswordHash != "argon2-hash" { t.Fatalf("unexpected user: %+v", got) } if _, err := s.GetUserByEmail(ctx, "nobody@example.com"); err == nil { t.Fatal("expected error for unknown email, got nil") } } // TestRegisterUser_DuplicateEmailReturnsErrEmailTaken verifies the fix for // the duplicate-registration gap: a second RegisterUser call for an // already-taken email must fail with the ErrEmailTaken sentinel (mapped from // the UNIQUE constraint violation on users.email), not a generic pgx error. func TestRegisterUser_DuplicateEmailReturnsErrEmailTaken(t *testing.T) { s, ctx := newStore(t) if _, _, err := s.RegisterUser(ctx, "dup@example.com", "argon2-hash"); err != nil { t.Fatal(err) } if _, _, err := s.RegisterUser(ctx, "dup@example.com", "argon2-hash"); !errors.Is(err, ErrEmailTaken) { t.Fatalf("expected ErrEmailTaken, got %v", err) } } // TestGetUserByID_ReturnsUser verifies the fix for the /me gap: GetUserByID // returns the same user created by RegisterUser, including their real email. func TestGetUserByID_ReturnsUser(t *testing.T) { s, ctx := newStore(t) u, _, err := s.RegisterUser(ctx, "gina@example.com", "argon2-hash") if err != nil { t.Fatal(err) } got, err := s.GetUserByID(ctx, u.ID) if err != nil { t.Fatal(err) } if got.ID != u.ID || got.Email != "gina@example.com" { t.Fatalf("unexpected user: %+v", got) } } // TestSessionLifecycle_CreateGetDelete verifies CreateSession + GetSessionUser // round-trips to the owning user ID, an expired session is excluded from // GetSessionUser, and DeleteSession removes the session. func TestSessionLifecycle_CreateGetDelete(t *testing.T) { s, ctx := newStore(t) u, _, err := s.RegisterUser(ctx, "carol@example.com", "argon2-hash") if err != nil { t.Fatal(err) } tokenHash := "sha256-token-hash" if err := s.CreateSession(ctx, u.ID, tokenHash, time.Now().Add(time.Hour)); err != nil { t.Fatal(err) } gotUserID, err := s.GetSessionUser(ctx, tokenHash) if err != nil { t.Fatal(err) } if gotUserID != u.ID { t.Fatalf("expected user %s, got %s", u.ID, gotUserID) } if err := s.DeleteSession(ctx, tokenHash); err != nil { t.Fatal(err) } if _, err := s.GetSessionUser(ctx, tokenHash); err == nil { t.Fatal("expected error after DeleteSession, got nil") } } // TestGetSessionUser_ExpiredSessionNotReturned verifies the query's // expires_at > now() condition: a session created with an expiry in the // past must not be returned by GetSessionUser. func TestGetSessionUser_ExpiredSessionNotReturned(t *testing.T) { s, ctx := newStore(t) u, _, err := s.RegisterUser(ctx, "dave@example.com", "argon2-hash") if err != nil { t.Fatal(err) } tokenHash := "expired-token-hash" if err := s.CreateSession(ctx, u.ID, tokenHash, time.Now().Add(-time.Hour)); err != nil { t.Fatal(err) } if _, err := s.GetSessionUser(ctx, tokenHash); err == nil { t.Fatal("expected expired session to not be returned, got nil error") } else if err != pgx.ErrNoRows { t.Fatalf("expected pgx.ErrNoRows, got %v", err) } } // TestGetProjectOwned_ForeignUserRejected verifies that looking up a project // with the wrong user ID fails, so one tenant cannot address another // tenant's project by guessing its ID. func TestGetProjectOwned_ForeignUserRejected(t *testing.T) { s, ctx := newStore(t) _, p, err := s.RegisterUser(ctx, "erin@example.com", "argon2-hash") if err != nil { t.Fatal(err) } foreignUserID := uuid.New() if _, err := s.GetProjectOwned(ctx, p.ID, foreignUserID); err == nil { t.Fatal("expected error for foreign user ID, got nil") } } // TestGetUserProject_ReturnsTheUsersProject verifies GetUserProject returns // the project created for that user by RegisterUser. func TestGetUserProject_ReturnsTheUsersProject(t *testing.T) { s, ctx := newStore(t) u, p, err := s.RegisterUser(ctx, "frank@example.com", "argon2-hash") if err != nil { t.Fatal(err) } got, err := s.GetUserProject(ctx, u.ID) if err != nil { t.Fatal(err) } if got.ID != p.ID { t.Fatalf("expected project %s, got %+v", p.ID, got) } }