package store import ( "path/filepath" "testing" ) func newTestStore(t *testing.T) *Store { t.Helper() dir := t.TempDir() st, err := New(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = st.Close() }) return st } func TestUserCreateVerify(t *testing.T) { st := newTestStore(t) u, err := st.CreateUser("alice", "secret-pass", "viewer") if err != nil { t.Fatalf("create: %v", err) } if u.Role != "viewer" || u.System || !u.Enabled { t.Fatalf("bad user: %+v", u) } // Wrong password must fail. if _, ok := st.VerifyUserPassword("alice", "wrong"); ok { t.Fatal("verify accepted wrong password") } // Correct password must succeed. got, ok := st.VerifyUserPassword("alice", "secret-pass") if !ok || got.Username != "alice" || got.Role != "viewer" { t.Fatalf("verify failed: %+v ok=%v", got, ok) } // Duplicate username must error. if _, err := st.CreateUser("alice", "x", "admin"); err == nil { t.Fatal("duplicate create should fail") } } func TestSystemUserSync(t *testing.T) { st := newTestStore(t) if err := st.SyncSystemUser("admin", "flag-pass"); err != nil { t.Fatalf("sync: %v", err) } u, ok := st.GetUser("admin") if !ok || !u.System || u.Role != "admin" { t.Fatalf("system user not synced: %+v ok=%v", u, ok) } // Flag password change propagates on re-sync. if err := st.SyncSystemUser("admin", "new-flag-pass"); err != nil { t.Fatalf("re-sync: %v", err) } if _, ok := st.VerifyUserPassword("admin", "flag-pass"); ok { t.Fatal("old flag password still works after re-sync") } if _, ok := st.VerifyUserPassword("admin", "new-flag-pass"); !ok { t.Fatal("new flag password rejected after re-sync") } } func TestLastAdminGuard(t *testing.T) { st := newTestStore(t) // System admin exists after sync; count must be 1. if err := st.SyncSystemUser("admin", "p"); err != nil { t.Fatal(err) } n, _ := st.CountAdmins() if n != 1 { t.Fatalf("admins = %d want 1", n) } // System user delete must be refused. if err := st.DeleteUser(1); err != ErrInvalid { t.Fatalf("delete system user: %v want ErrInvalid", err) } // Add a viewer, then disable the system admin: still 1 admin, allowed at // store level (handler enforces the guard, store just refuses system rows). if _, err := st.CreateUser("bob", "p", "viewer"); err != nil { t.Fatal(err) } } func TestApiKeyCreateLookup(t *testing.T) { st := newTestStore(t) u, _ := st.CreateUser("carol", "p", "admin") k, plaintext, err := st.CreateApiKey(u.ID, "ci", "write") if err != nil { t.Fatalf("create key: %v", err) } if plaintext == "" || k.Prefix == "" { t.Fatalf("empty plaintext/prefix: %+v", k) } if plaintext[:8] != k.Prefix { t.Fatalf("prefix mismatch: %q vs %q", k.Prefix, plaintext[:8]) } // Lookup via the sha256 of the plaintext must find the key + owner. found, owner, ok := st.LookupApiKey(HashApiKey(plaintext)) if !ok || owner.ID != u.ID || found.Scope != "write" { t.Fatalf("lookup failed: %+v owner=%+v ok=%v", found, owner, ok) } // A bogus hash must miss. if _, _, ok := st.LookupApiKey(HashApiKey("not-a-real-key")); ok { t.Fatal("bogus key should not be found") } // Deleting the user cascades to the key (FK ON DELETE CASCADE). if err := st.DeleteUser(u.ID); err != nil { t.Fatalf("delete user: %v", err) } if _, _, ok := st.LookupApiKey(HashApiKey(plaintext)); ok { t.Fatal("key survived user deletion") } }