package store import ( "context" "errors" "fmt" "os" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/baicai2026-baicai/goods/api/internal/apikey" ) func testStore(t *testing.T) *Store { t.Helper() dsn := os.Getenv("OPENGOODS_DATABASE_URL") if dsn == "" { dsn = "postgres://opengoods:opengoods@localhost:5432/opengoods?sslmode=disable" } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Skipf("no database: %v", err) } if err := pool.Ping(ctx); err != nil { pool.Close() t.Skipf("database not reachable: %v", err) } var hasUser bool if err := pool.QueryRow(ctx, "SELECT to_regclass('public.app_user') IS NOT NULL").Scan(&hasUser); err != nil || !hasUser { pool.Close() t.Skip("migrations not applied") } t.Cleanup(pool.Close) return New(pool) } func TestRegisterAuthenticateRegenerate(t *testing.T) { s := testStore(t) ctx := context.Background() email := fmt.Sprintf("user-%d@example.com", time.Now().UnixNano()) t.Cleanup(func() { _, _ = s.pool.Exec(ctx, "DELETE FROM app_user WHERE lower(email)=lower($1)", email) _, _ = s.pool.Exec(ctx, "DELETE FROM api_key WHERE owner_email=$1", email) }) key, acct, err := s.RegisterUser(ctx, email, "supersecret", 300, 100000) if err != nil { t.Fatalf("register: %v", err) } if key == "" || acct.KeyPrefix == "" || acct.QuotaTotal != 100000 || acct.RateLimitPerMin != 300 { t.Fatalf("unexpected account: %+v key=%q", acct, key) } // The issued key resolves via the public auth path with its quota attached. k, err := s.APIKeyByHash(ctx, apikey.Hash(key)) if err != nil { t.Fatalf("APIKeyByHash: %v", err) } if k.QuotaTotal != 100000 || k.RateLimitPerMin != 300 { t.Fatalf("key metadata mismatch: %+v", k) } // Duplicate email is rejected. if _, _, err := s.RegisterUser(ctx, email, "anotherpass", 300, 100000); !errors.Is(err, ErrEmailTaken) { t.Fatalf("expected ErrEmailTaken, got %v", err) } // Wrong password fails; correct password authenticates. if _, err := s.Authenticate(ctx, email, "wrong"); !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for bad password, got %v", err) } got, err := s.Authenticate(ctx, email, "supersecret") if err != nil { t.Fatalf("authenticate: %v", err) } if got.KeyPrefix != acct.KeyPrefix { t.Fatalf("authenticate key prefix = %q want %q", got.KeyPrefix, acct.KeyPrefix) } // Regeneration revokes the old key and issues a new one. newKey, regen, oldKeyID, err := s.RegenerateKey(ctx, email, "supersecret", 300, 100000) if err != nil { t.Fatalf("regenerate: %v", err) } if newKey == key || oldKeyID != acct.KeyID || regen.KeyID == oldKeyID { t.Fatalf("regenerate did not rotate key: old=%s new=%+v", oldKeyID, regen) } if _, err := s.APIKeyByHash(ctx, apikey.Hash(key)); !errors.Is(err, ErrNotFound) { t.Fatalf("old key should be revoked, got %v", err) } if _, err := s.APIKeyByHash(ctx, apikey.Hash(newKey)); err != nil { t.Fatalf("new key should be active: %v", err) } }