dev-pod-api-build/internal/store/users_test.go
2026-04-16 04:16:36 +00:00

442 lines
10 KiB
Go

package store
import (
"context"
"errors"
"strings"
"testing"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
)
// --- User CRUD tests ---
func TestCreateUser(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
quota := model.DefaultQuota()
u, err := s.CreateUser(ctx, "alice", quota)
if err != nil {
t.Fatalf("create user: %v", err)
}
if u.ID != "alice" {
t.Errorf("got id %q, want %q", u.ID, "alice")
}
if u.CreatedAt.IsZero() {
t.Error("created_at should not be zero")
}
if u.Quota != quota {
t.Errorf("got quota %+v, want %+v", u.Quota, quota)
}
}
func TestCreateUserDuplicate(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
quota := model.DefaultQuota()
if _, err := s.CreateUser(ctx, "alice", quota); err != nil {
t.Fatalf("first create: %v", err)
}
_, err := s.CreateUser(ctx, "alice", quota)
if err == nil {
t.Fatal("expected error for duplicate user")
}
if !errors.Is(err, ErrDuplicate) {
t.Errorf("got %v, want ErrDuplicate", err)
}
}
func TestGetUser(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
quota := model.DefaultQuota()
created, err := s.CreateUser(ctx, "bob", quota)
if err != nil {
t.Fatalf("create: %v", err)
}
got, err := s.GetUser(ctx, "bob")
if err != nil {
t.Fatalf("get: %v", err)
}
if got.ID != created.ID {
t.Errorf("got id %q, want %q", got.ID, created.ID)
}
if got.Quota != quota {
t.Errorf("got quota %+v, want %+v", got.Quota, quota)
}
}
func TestGetUserNotFound(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
_, err := s.GetUser(ctx, "nonexistent")
if err == nil {
t.Fatal("expected error")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestListUsers(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
quota := model.DefaultQuota()
// Empty list
users, err := s.ListUsers(ctx)
if err != nil {
t.Fatalf("list empty: %v", err)
}
if len(users) != 0 {
t.Errorf("got %d users, want 0", len(users))
}
// Create users
if _, err := s.CreateUser(ctx, "alice", quota); err != nil {
t.Fatalf("create alice: %v", err)
}
if _, err := s.CreateUser(ctx, "bob", quota); err != nil {
t.Fatalf("create bob: %v", err)
}
users, err = s.ListUsers(ctx)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(users) != 2 {
t.Fatalf("got %d users, want 2", len(users))
}
// Ordered by created_at
if users[0].ID != "alice" {
t.Errorf("first user: got %q, want %q", users[0].ID, "alice")
}
if users[1].ID != "bob" {
t.Errorf("second user: got %q, want %q", users[1].ID, "bob")
}
}
func TestDeleteUser(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create: %v", err)
}
if err := s.DeleteUser(ctx, "alice"); err != nil {
t.Fatalf("delete: %v", err)
}
// Should be gone
_, err := s.GetUser(ctx, "alice")
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v after delete, want ErrNotFound", err)
}
}
func TestDeleteUserNotFound(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
err := s.DeleteUser(ctx, "ghost")
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestDeleteUserCascadesAPIKeys(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create user: %v", err)
}
plain, hash, err := GenerateAPIKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
if err := s.CreateAPIKey(ctx, "alice", model.RoleUser, hash); err != nil {
t.Fatalf("create key: %v", err)
}
// Delete user should cascade to api_keys
if err := s.DeleteUser(ctx, "alice"); err != nil {
t.Fatalf("delete: %v", err)
}
// Key should no longer validate
_, _, err = s.ValidateKey(ctx, plain)
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v after cascade delete, want ErrNotFound", err)
}
}
// --- Quota tests ---
func TestUpdateQuotas(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create: %v", err)
}
newPods := 5
newCPU := 16
updated, err := s.UpdateQuotas(ctx, "alice", model.UpdateQuotasRequest{
MaxConcurrentPods: &newPods,
MaxCPUPerPod: &newCPU,
})
if err != nil {
t.Fatalf("update quotas: %v", err)
}
if updated.Quota.MaxConcurrentPods != 5 {
t.Errorf("max_concurrent_pods: got %d, want 5", updated.Quota.MaxConcurrentPods)
}
if updated.Quota.MaxCPUPerPod != 16 {
t.Errorf("max_cpu_per_pod: got %d, want 16", updated.Quota.MaxCPUPerPod)
}
// Unchanged fields should keep defaults
if updated.Quota.MaxRAMGBPerPod != 16 {
t.Errorf("max_ram_gb_per_pod: got %d, want 16", updated.Quota.MaxRAMGBPerPod)
}
}
func TestUpdateQuotasNoChanges(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create: %v", err)
}
// Empty update should return current user
u, err := s.UpdateQuotas(ctx, "alice", model.UpdateQuotasRequest{})
if err != nil {
t.Fatalf("update no changes: %v", err)
}
if u.Quota != model.DefaultQuota() {
t.Errorf("quota changed unexpectedly: %+v", u.Quota)
}
}
func TestUpdateQuotasNotFound(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
newPods := 5
_, err := s.UpdateQuotas(ctx, "ghost", model.UpdateQuotasRequest{
MaxConcurrentPods: &newPods,
})
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestQuotaStorageAndRetrieval(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
customQuota := model.Quota{
MaxConcurrentPods: 10,
MaxCPUPerPod: 32,
MaxRAMGBPerPod: 64,
MonthlyPodHours: 1000,
MonthlyAIRequests: 50000,
}
if _, err := s.CreateUser(ctx, "power-user", customQuota); err != nil {
t.Fatalf("create: %v", err)
}
got, err := s.GetUser(ctx, "power-user")
if err != nil {
t.Fatalf("get: %v", err)
}
if got.Quota != customQuota {
t.Errorf("quota mismatch:\ngot: %+v\nwant: %+v", got.Quota, customQuota)
}
}
// --- API key tests ---
func TestGenerateAPIKey(t *testing.T) {
plain, hash, err := GenerateAPIKey()
if err != nil {
t.Fatalf("generate: %v", err)
}
// Check prefix
if !strings.HasPrefix(plain, model.APIKeyPrefix) {
t.Errorf("key %q missing prefix %q", plain, model.APIKeyPrefix)
}
// Check length: dpk_ (4) + 24 hex chars = 28
if len(plain) != 28 {
t.Errorf("key length: got %d, want 28", len(plain))
}
// Hash should be consistent
if HashKey(plain) != hash {
t.Error("hash mismatch")
}
// Two keys should be different
plain2, _, err := GenerateAPIKey()
if err != nil {
t.Fatalf("generate second: %v", err)
}
if plain == plain2 {
t.Error("two generated keys should not be equal")
}
}
func TestHashKeyConsistency(t *testing.T) {
key := "dpk_abcdef1234567890abcdef12"
h1 := HashKey(key)
h2 := HashKey(key)
if h1 != h2 {
t.Error("hash should be deterministic")
}
// SHA-256 produces 64 hex chars
if len(h1) != 64 {
t.Errorf("hash length: got %d, want 64", len(h1))
}
}
func TestCreateAndValidateAPIKey(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create user: %v", err)
}
plain, hash, err := GenerateAPIKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
if err := s.CreateAPIKey(ctx, "alice", model.RoleUser, hash); err != nil {
t.Fatalf("create key: %v", err)
}
// Validate returns correct user and role
u, role, err := s.ValidateKey(ctx, plain)
if err != nil {
t.Fatalf("validate: %v", err)
}
if u.ID != "alice" {
t.Errorf("user: got %q, want %q", u.ID, "alice")
}
if role != model.RoleUser {
t.Errorf("role: got %q, want %q", role, model.RoleUser)
}
}
func TestValidateAdminKey(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "admin1", model.DefaultQuota()); err != nil {
t.Fatalf("create user: %v", err)
}
plain, hash, err := GenerateAPIKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
if err := s.CreateAPIKey(ctx, "admin1", model.RoleAdmin, hash); err != nil {
t.Fatalf("create key: %v", err)
}
_, role, err := s.ValidateKey(ctx, plain)
if err != nil {
t.Fatalf("validate: %v", err)
}
if role != model.RoleAdmin {
t.Errorf("role: got %q, want %q", role, model.RoleAdmin)
}
}
func TestValidateInvalidKey(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
_, _, err := s.ValidateKey(ctx, "dpk_invalid_key_here_nope")
if err == nil {
t.Fatal("expected error for invalid key")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestMultipleKeysPerUser(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create user: %v", err)
}
// Create two keys
plain1, hash1, _ := GenerateAPIKey()
plain2, hash2, _ := GenerateAPIKey()
if err := s.CreateAPIKey(ctx, "alice", model.RoleUser, hash1); err != nil {
t.Fatalf("create key 1: %v", err)
}
if err := s.CreateAPIKey(ctx, "alice", model.RoleAdmin, hash2); err != nil {
t.Fatalf("create key 2: %v", err)
}
// Both should validate
u1, role1, err := s.ValidateKey(ctx, plain1)
if err != nil {
t.Fatalf("validate key 1: %v", err)
}
if u1.ID != "alice" || role1 != model.RoleUser {
t.Errorf("key1: got user=%q role=%q, want alice/user", u1.ID, role1)
}
u2, role2, err := s.ValidateKey(ctx, plain2)
if err != nil {
t.Fatalf("validate key 2: %v", err)
}
if u2.ID != "alice" || role2 != model.RoleAdmin {
t.Errorf("key2: got user=%q role=%q, want alice/admin", u2.ID, role2)
}
}
func TestValidateKeyUpdatesLastUsed(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if _, err := s.CreateUser(ctx, "alice", model.DefaultQuota()); err != nil {
t.Fatalf("create user: %v", err)
}
plain, hash, _ := GenerateAPIKey()
if err := s.CreateAPIKey(ctx, "alice", model.RoleUser, hash); err != nil {
t.Fatalf("create key: %v", err)
}
// Validate should update last_used_at
if _, _, err := s.ValidateKey(ctx, plain); err != nil {
t.Fatalf("validate: %v", err)
}
// Check last_used_at is set
var lastUsed *string
err := testPool.QueryRow(ctx,
`SELECT last_used_at::text FROM api_keys WHERE key_hash = $1`, hash).Scan(&lastUsed)
if err != nil {
t.Fatalf("query last_used_at: %v", err)
}
if lastUsed == nil {
t.Error("last_used_at should be set after validation")
}
}