//go:build integration // Integration tests run against a real PostgreSQL instance supplied through // TEST_DATABASE_URL, with the userservice migrations already applied. A real DB // is required because the guarantees under test — the unique-email constraint // mapping to ErrEmailExists, the ck_users_email_lower check — are properties of // Postgres, not of the Go code. // // docker compose up -d // TEST_DATABASE_URL='postgres://scimesh:scimesh@localhost:5432/scimesh?sslmode=disable' \ // go test -tags=integration ./internal/storage/postgres/ -v package postgres import ( "context" "errors" "fmt" "os" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/emil28092005/SciMesh/users/internal/domain" "github.com/emil28092005/SciMesh/users/internal/usecase" ) func testPool(t *testing.T) *pgxpool.Pool { t.Helper() url := os.Getenv("TEST_DATABASE_URL") if url == "" { t.Skip("TEST_DATABASE_URL is not set") } pool, err := pgxpool.New(context.Background(), url) if err != nil { t.Fatalf("connect: %v", err) } t.Cleanup(pool.Close) return pool } // seedUser inserts a user with a unique email and removes it afterwards, so // tests stay independent of each other and of leftovers from earlier runs. func seedUser(t *testing.T, repo *UserRepo) *domain.User { t.Helper() email := fmt.Sprintf("it-%s@example.com", uuid.NewString()) u, err := domain.NewUser(email, "$2a$04$abcdefghijklmnopqrstuv", time.Now().UTC()) if err != nil { t.Fatalf("build user: %v", err) } if err := repo.Insert(context.Background(), u); err != nil { t.Fatalf("insert: %v", err) } t.Cleanup(func() { _, _ = repo.pool.Exec(context.Background(), "DELETE FROM users WHERE id = $1", u.ID) }) return u } func TestUserRepoInsertAndGet(t *testing.T) { repo := NewUserRepo(testPool(t)) ctx := context.Background() want := seedUser(t, repo) byEmail, err := repo.GetByEmail(ctx, want.Email) if err != nil { t.Fatalf("GetByEmail: %v", err) } if byEmail.ID != want.ID || byEmail.Email != want.Email || byEmail.Role != domain.RoleUser { t.Errorf("GetByEmail mismatch: %+v", byEmail) } byID, err := repo.GetByID(ctx, want.ID) if err != nil { t.Fatalf("GetByID: %v", err) } if byID.Email != want.Email { t.Errorf("GetByID mismatch: %+v", byID) } } func TestUserRepoDuplicateEmail(t *testing.T) { repo := NewUserRepo(testPool(t)) existing := seedUser(t, repo) // A second user with the same email must hit the unique constraint and map // to the port's sentinel error. dup, err := domain.NewUser(existing.Email, "$2a$04$abcdefghijklmnopqrstuv", time.Now().UTC()) if err != nil { t.Fatal(err) } err = repo.Insert(context.Background(), dup) if !errors.Is(err, usecase.ErrEmailExists) { t.Errorf("got %v, want ErrEmailExists", err) } } func TestUserRepoNotFound(t *testing.T) { repo := NewUserRepo(testPool(t)) ctx := context.Background() if _, err := repo.GetByID(ctx, uuid.New()); !errors.Is(err, usecase.ErrUserNotFound) { t.Errorf("GetByID unknown: got %v, want ErrUserNotFound", err) } if _, err := repo.GetByEmail(ctx, "ghost@example.com"); !errors.Is(err, usecase.ErrUserNotFound) { t.Errorf("GetByEmail unknown: got %v, want ErrUserNotFound", err) } } func TestUserRepoSetVerified(t *testing.T) { repo := NewUserRepo(testPool(t)) ctx := context.Background() u := seedUser(t, repo) // A fresh row defaults to unverified. got, err := repo.GetByID(ctx, u.ID) if err != nil { t.Fatal(err) } if got.Verified { t.Fatal("new user must default to unverified") } if err := repo.SetVerified(ctx, u.ID, true); err != nil { t.Fatalf("grant: %v", err) } got, _ = repo.GetByID(ctx, u.ID) if !got.Verified { t.Error("verified flag not persisted") } if err := repo.SetVerified(ctx, u.ID, false); err != nil { t.Fatalf("revoke: %v", err) } got, _ = repo.GetByID(ctx, u.ID) if got.Verified { t.Error("verified flag not cleared") } } func TestUserRepoSetVerifiedUnknown(t *testing.T) { repo := NewUserRepo(testPool(t)) if err := repo.SetVerified(context.Background(), uuid.New(), true); !errors.Is(err, usecase.ErrUserNotFound) { t.Errorf("got %v, want ErrUserNotFound", err) } } func TestUserRepoSetRole(t *testing.T) { repo := NewUserRepo(testPool(t)) ctx := context.Background() u := seedUser(t, repo) if err := repo.SetRole(ctx, u.ID, domain.RoleAdmin); err != nil { t.Fatalf("promote: %v", err) } got, _ := repo.GetByID(ctx, u.ID) if got.Role != domain.RoleAdmin { t.Errorf("role = %q, want admin", got.Role) } if err := repo.SetRole(ctx, uuid.New(), domain.RoleAdmin); !errors.Is(err, usecase.ErrUserNotFound) { t.Errorf("unknown user: got %v, want ErrUserNotFound", err) } }