This commit is contained in:
iceBear67
2026-08-15 07:13:00 +00:00
commit dd50674fdc
114 changed files with 26865 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
package store
import (
"bytes"
"context"
"errors"
"fmt"
"testing"
"time"
)
func mkKey(t *testing.T, db *DB, id string, scope Scope, projectID *int64) *APIKey {
t.Helper()
hash := bytes.Repeat([]byte{byte(len(id))}, 32)
k := &APIKey{ID: id, SecretHash: hash, Scope: scope, ProjectID: projectID, Name: "test " + id}
if err := db.CreateKey(context.Background(), k); err != nil {
t.Fatalf("CreateKey(%s): %v", id, err)
}
return k
}
func TestCreateAndReadKey(t *testing.T) {
ctx := context.Background()
db := testDB(t)
p := DefaultProject("demo")
if err := db.CreateProject(ctx, p); err != nil {
t.Fatal(err)
}
admin := mkKey(t, db, "adminkeyid000000", ScopeAdmin, nil)
proj := mkKey(t, db, "projkeyid0000000", ScopeProject, &p.ID)
got, err := db.KeyByID(ctx, admin.ID)
if err != nil {
t.Fatalf("KeyByID: %v", err)
}
if got.Scope != ScopeAdmin {
t.Errorf("scope = %q, want admin", got.Scope)
}
if got.ProjectID != nil {
t.Errorf("admin key has project_id %v", *got.ProjectID)
}
if !bytes.Equal(got.SecretHash, admin.SecretHash) {
t.Error("secret hash did not round trip")
}
if got.CreatedAt.IsZero() {
t.Error("created_at not set")
}
if got.RevokedAt != nil || got.ExpiresAt != nil || got.LastUsedAt != nil {
t.Errorf("optional timestamps should be nil: %+v", got)
}
got, err = db.KeyByID(ctx, proj.ID)
if err != nil {
t.Fatal(err)
}
if got.ProjectID == nil || *got.ProjectID != p.ID {
t.Errorf("project key lost its project: %+v", got)
}
}
func TestKeyNotFound(t *testing.T) {
db := testDB(t)
if _, err := db.KeyByID(context.Background(), "missing000000000"); !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestCreateKeyDuplicateID(t *testing.T) {
ctx := context.Background()
db := testDB(t)
mkKey(t, db, "adminkeyid000000", ScopeAdmin, nil)
err := db.CreateKey(ctx, &APIKey{ID: "adminkeyid000000", SecretHash: make([]byte, 32), Scope: ScopeAdmin})
if !errors.Is(err, ErrExists) {
t.Fatalf("got %v, want ErrExists", err)
}
}
func TestRevokeKey(t *testing.T) {
ctx := context.Background()
db := testDB(t)
k := mkKey(t, db, "adminkeyid000000", ScopeAdmin, nil)
if err := db.RevokeKey(ctx, k.ID); err != nil {
t.Fatalf("RevokeKey: %v", err)
}
got, err := db.KeyByID(ctx, k.ID)
if err != nil {
t.Fatal(err)
}
if got.RevokedAt == nil {
t.Fatal("revoked_at not set")
}
first := *got.RevokedAt
if got.Usable(time.Now()) {
t.Error("a revoked key must not be usable")
}
// Revoking again must be a no-op, not a moved timestamp: the first time is
// when the key actually stopped working.
if err := db.RevokeKey(ctx, k.ID); err != nil {
t.Fatalf("second RevokeKey: %v", err)
}
got, err = db.KeyByID(ctx, k.ID)
if err != nil {
t.Fatal(err)
}
if !got.RevokedAt.Equal(first) {
t.Errorf("revoked_at moved from %v to %v", first, *got.RevokedAt)
}
if err := db.RevokeKey(ctx, "missing000000000"); !errors.Is(err, ErrNotFound) {
t.Errorf("revoking an unknown key: got %v, want ErrNotFound", err)
}
}
func TestKeyUsable(t *testing.T) {
now := time.Unix(1_000_000, 0).UTC()
past := now.Add(-time.Hour)
future := now.Add(time.Hour)
cases := []struct {
name string
key APIKey
wantUse bool
}{
{"fresh", APIKey{}, true},
{"revoked", APIKey{RevokedAt: &past}, false},
{"expired", APIKey{ExpiresAt: &past}, false},
{"expires later", APIKey{ExpiresAt: &future}, true},
{"expires exactly now", APIKey{ExpiresAt: &now}, false},
{"revoked and unexpired", APIKey{RevokedAt: &past, ExpiresAt: &future}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.key.Usable(now); got != tc.wantUse {
t.Errorf("Usable = %v, want %v", got, tc.wantUse)
}
})
}
}
func TestListKeys(t *testing.T) {
ctx := context.Background()
db := testDB(t)
a := DefaultProject("alpha")
b := DefaultProject("beta")
if err := db.CreateProject(ctx, a); err != nil {
t.Fatal(err)
}
if err := db.CreateProject(ctx, b); err != nil {
t.Fatal(err)
}
mkKey(t, db, "adminkeyid000000", ScopeAdmin, nil)
mkKey(t, db, "alphakey00000001", ScopeProject, &a.ID)
mkKey(t, db, "alphakey00000002", ScopeProject, &a.ID)
mkKey(t, db, "betakey000000001", ScopeProject, &b.ID)
all, err := db.ListKeys(ctx, nil)
if err != nil {
t.Fatal(err)
}
if len(all) != 4 {
t.Errorf("ListKeys(nil) returned %d keys, want 4", len(all))
}
forA, err := db.ListKeys(ctx, &a.ID)
if err != nil {
t.Fatal(err)
}
if len(forA) != 2 {
t.Fatalf("ListKeys(alpha) returned %d keys, want 2", len(forA))
}
for _, k := range forA {
if k.ProjectID == nil || *k.ProjectID != a.ID {
t.Errorf("key %s leaked into alpha's list", k.ID)
}
}
// Revoked keys stay listed so an operator can see what was revoked and when.
if err := db.RevokeKey(ctx, "alphakey00000001"); err != nil {
t.Fatal(err)
}
forA, err = db.ListKeys(ctx, &a.ID)
if err != nil {
t.Fatal(err)
}
if len(forA) != 2 {
t.Errorf("after revoke, ListKeys(alpha) returned %d keys, want 2", len(forA))
}
}
// A zero result here is what makes the server mint a bootstrap token, so the
// "usable" definition has to match what the verifier will accept.
func TestCountUsableAdminKeys(t *testing.T) {
ctx := context.Background()
db := testDB(t)
p := DefaultProject("demo")
if err := db.CreateProject(ctx, p); err != nil {
t.Fatal(err)
}
if n, err := db.CountUsableAdminKeys(ctx); err != nil || n != 0 {
t.Fatalf("empty database: n=%d err=%v, want 0", n, err)
}
// A project key is not an admin key.
mkKey(t, db, "projkeyid0000000", ScopeProject, &p.ID)
if n, _ := db.CountUsableAdminKeys(ctx); n != 0 {
t.Errorf("project key counted as admin: n=%d", n)
}
mkKey(t, db, "adminkeyid000000", ScopeAdmin, nil)
if n, _ := db.CountUsableAdminKeys(ctx); n != 1 {
t.Errorf("n=%d, want 1", n)
}
// An expired admin key must not keep the server from bootstrapping.
expired := time.Now().Add(-time.Hour)
if err := db.CreateKey(ctx, &APIKey{
ID: "expiredadmin0000", SecretHash: make([]byte, 32), Scope: ScopeAdmin, ExpiresAt: &expired,
}); err != nil {
t.Fatal(err)
}
if n, _ := db.CountUsableAdminKeys(ctx); n != 1 {
t.Errorf("expired admin key counted: n=%d", n)
}
if err := db.RevokeKey(ctx, "adminkeyid000000"); err != nil {
t.Fatal(err)
}
if n, _ := db.CountUsableAdminKeys(ctx); n != 0 {
t.Errorf("revoked admin key counted: n=%d", n)
}
}
func TestTouchKeys(t *testing.T) {
ctx := context.Background()
db := testDB(t)
mkKey(t, db, "key0000000000001", ScopeAdmin, nil)
mkKey(t, db, "key0000000000002", ScopeAdmin, nil)
if err := db.TouchKeys(ctx, nil); err != nil {
t.Errorf("empty batch should be a no-op: %v", err)
}
t1 := time.Unix(1_700_000_000, 0)
if err := db.TouchKeys(ctx, map[string]time.Time{
"key0000000000001": t1,
"key0000000000002": t1,
// A key that vanished between the request and the flush must not fail
// the whole batch, or one deleted key would stall the flusher forever.
"deletedkey000000": t1,
}); err != nil {
t.Fatalf("TouchKeys: %v", err)
}
lastUsed := func(id string) *time.Time {
t.Helper()
k, err := db.KeyByID(ctx, id)
if err != nil {
t.Fatal(err)
}
return k.LastUsedAt
}
if got := lastUsed("key0000000000001"); got == nil || !got.Equal(t1.UTC()) {
t.Errorf("last_used_at = %v, want %v", got, t1.UTC())
}
// Batches can arrive out of order once the flusher runs concurrently with a
// retry; an older timestamp must not walk the column backwards.
older := t1.Add(-time.Hour)
if err := db.TouchKeys(ctx, map[string]time.Time{"key0000000000001": older}); err != nil {
t.Fatal(err)
}
if got := lastUsed("key0000000000001"); !got.Equal(t1.UTC()) {
t.Errorf("last_used_at moved backwards to %v", got)
}
newer := t1.Add(time.Hour)
if err := db.TouchKeys(ctx, map[string]time.Time{"key0000000000001": newer}); err != nil {
t.Fatal(err)
}
if got := lastUsed("key0000000000001"); !got.Equal(newer.UTC()) {
t.Errorf("last_used_at = %v, want %v", got, newer.UTC())
}
}
// The write pool holds a single connection, so a batch flush must not need more
// than one; this would deadlock if TouchKeys opened a nested transaction.
func TestTouchKeysLargeBatch(t *testing.T) {
ctx := context.Background()
db := testDB(t)
seen := map[string]time.Time{}
now := time.Unix(1_700_000_000, 0)
for i := 0; i < 200; i++ {
id := fmt.Sprintf("key%013d", i)
mkKey(t, db, id, ScopeAdmin, nil)
seen[id] = now
}
if err := db.TouchKeys(ctx, seen); err != nil {
t.Fatalf("TouchKeys: %v", err)
}
var n int
if err := db.Reader().QueryRow(
`SELECT count(*) FROM api_keys WHERE last_used_at = ?`, now.Unix()).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 200 {
t.Errorf("%d keys touched, want 200", n)
}
}
// Keys must die with their project, or a project name could be recreated and
// inherit the old owner's credentials.
func TestKeysCascadeWithProject(t *testing.T) {
ctx := context.Background()
db := testDB(t)
p := DefaultProject("demo")
if err := db.CreateProject(ctx, p); err != nil {
t.Fatal(err)
}
mkKey(t, db, "projkeyid0000000", ScopeProject, &p.ID)
if err := db.DeleteProject(ctx, p.ID); err != nil {
t.Fatal(err)
}
if _, err := db.KeyByID(ctx, "projkeyid0000000"); !errors.Is(err, ErrNotFound) {
t.Errorf("key survived its project: %v", err)
}
}