Files

186 lines
5.2 KiB
Go

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)
}
}