Files
ResolveSpec/pkg/security/lookup/direct/keys_test.go
T
Hein c9fa8c60f2 refactor(security): move all database access into pkg/security/lookup
pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).

- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
  podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
  parameter, JSON null arrays, expires_at timezone casts, passkey list
  GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
2026-10-01 13:19:44 +02:00

72 lines
2.6 KiB
Go

package direct
import (
"context"
"testing"
"time"
"github.com/bitechdev/ResolveSpec/pkg/security/sectypes"
)
func TestKeysLifecycle(t *testing.T) {
ctx := context.Background()
db := newTestDB(t)
k := NewKeys(newTestBase(t, db, nil))
_, _ = db.Exec(`INSERT INTO users (username,email,password,is_active) VALUES ('u','u@x.io','x',1)`)
exp := time.Now().Add(time.Hour)
created, err := k.Create(ctx, sectypes.CreateKeyRequest{
UserID: 1, KeyType: sectypes.KeyTypeHeaderAPI, Name: "ci",
Scopes: []string{"read", "write"}, Meta: map[string]any{"env": "prod"}, ExpiresAt: &exp,
}, sectypes.HashKey("raw1"))
if err != nil || created.ID == 0 {
t.Fatalf("%+v %v", created, err)
}
// nil scopes/meta must not store the JSON text "null"
if _, err := k.Create(ctx, sectypes.CreateKeyRequest{UserID: 1, KeyType: sectypes.KeyTypeJWTSecret, Name: "j"}, sectypes.HashKey("raw2")); err != nil {
t.Fatal(err)
}
if _, err := k.Create(ctx, sectypes.CreateKeyRequest{UserID: 1, KeyType: sectypes.KeyTypeGenericAPI, Name: "old", ExpiresAt: ptr(time.Now().Add(-time.Hour))}, sectypes.HashKey("raw3")); err != nil {
t.Fatal(err)
}
all, err := k.List(ctx, 1, "")
if err != nil || len(all) != 2 {
t.Fatalf("list all: %d %v", len(all), err)
}
one, _ := k.List(ctx, 1, sectypes.KeyTypeHeaderAPI)
if len(one) != 1 || one[0].Name != "ci" || len(one[0].Scopes) != 2 || one[0].Meta["env"] != "prod" || one[0].ExpiresAt == nil {
t.Fatalf("list typed: %+v", one)
}
if other, _ := k.List(ctx, 2, ""); len(other) != 0 {
t.Fatal("other user's keys listed")
}
got, err := k.Validate(ctx, sectypes.HashKey("raw1"), sectypes.KeyTypeHeaderAPI)
if err != nil || got.UserID != 1 || got.KeyHash != sectypes.HashKey("raw1") || got.LastUsedAt == nil {
t.Fatalf("%+v %v", got, err)
}
if _, err := k.Validate(ctx, sectypes.HashKey("raw1"), sectypes.KeyTypeGenericAPI); err == nil || err.Error() != "invalid or expired key" {
t.Fatalf("wrong type: %v", err)
}
if _, err := k.Validate(ctx, sectypes.HashKey("raw3"), ""); err == nil {
t.Fatal("expired key validated")
}
if _, err := k.Delete(ctx, 2, created.ID); err == nil || err.Error() != "key not found or already deleted" {
t.Fatalf("foreign delete: %v", err)
}
hash, err := k.Delete(ctx, 1, created.ID)
if err != nil || hash != sectypes.HashKey("raw1") {
t.Fatalf("%q %v", hash, err)
}
if _, err := k.Delete(ctx, 1, created.ID); err == nil {
t.Fatal("double delete succeeded")
}
if _, err := k.Validate(ctx, sectypes.HashKey("raw1"), ""); err == nil {
t.Fatal("deleted key validated")
}
}
func ptr[T any](v T) *T { return &v }