iop/apps/control-plane/internal/credentialstore/projection_test.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

219 lines
6.8 KiB
Go

package credentialstore
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"time"
"google.golang.org/protobuf/proto"
)
func TestProjectionGenerationCommitsWithMutation(t *testing.T) {
ctx := context.Background()
registry := newFakeKeyRegistry()
registry.RegisterKey("k1", 1)
store, err := Open(ctx, filepath.Join(t.TempDir(), "projection-generation.db"), WithEnvelopeKeyRegistry(registry))
if err != nil {
t.Fatal(err)
}
defer store.Close()
assertGeneration := func(want uint64) {
t.Helper()
got, err := store.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("generation: got %d want %d", got, want)
}
}
assertGeneration(0)
issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "projection-principal"})
if err != nil {
t.Fatal(err)
}
assertGeneration(1)
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "projection-principal"}); !errors.Is(err, ErrPrincipalAlreadyExists) {
t.Fatalf("duplicate create: got %v", err)
}
assertGeneration(1)
slot, err := store.CreateSlot(ctx, CreateSlotInput{
PrincipalID: issued.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer,
Alias: "primary", Envelope: SecretEnvelope{
Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1,
Nonce: []byte("123456789012"), Ciphertext: []byte("opaque-ciphertext"),
},
})
if err != nil {
t.Fatal(err)
}
assertGeneration(2)
route, err := store.CreateRoute(ctx, CreateRouteInput{
PrincipalID: issued.Principal.ID, SlotID: slot.ID, Alias: "model-primary",
ProfileID: "openai", UpstreamModel: "upstream-model",
})
if err != nil {
t.Fatal(err)
}
assertGeneration(3)
if _, err := store.DisableRoute(ctx, issued.Principal.ID, route.ID, route.Revision); err != nil {
t.Fatal(err)
}
assertGeneration(4)
currentSlot, err := store.GetSlot(ctx, issued.Principal.ID, slot.ID)
if err != nil {
t.Fatal(err)
}
rotatedSlot, err := store.RotateSlotSecret(ctx, RotateSlotSecretInput{
PrincipalID: issued.Principal.ID, SlotID: slot.ID, CurrentRevision: currentSlot.Revision,
Envelope: SecretEnvelope{
Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1,
Nonce: []byte("abcdefghijkl"), Ciphertext: []byte("rotated-opaque-ciphertext"),
},
})
if err != nil {
t.Fatal(err)
}
assertGeneration(5)
if _, err := store.DisableSlot(ctx, issued.Principal.ID, slot.ID, rotatedSlot.Revision); err != nil {
t.Fatal(err)
}
assertGeneration(6)
if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
t.Fatal(err)
}
assertGeneration(7)
}
func TestProjectionGenerationPersistsAcrossReopen(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "projection-reopen.db")
store, err := Open(ctx, dbPath)
if err != nil {
t.Fatal(err)
}
issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "persistent-projection"})
if err != nil {
t.Fatal(err)
}
want, err := store.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
if err := store.Close(); err != nil {
t.Fatal(err)
}
reopened, err := Open(ctx, dbPath)
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
got, err := reopened.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("persisted generation: got %d want %d", got, want)
}
if _, err := reopened.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
t.Fatal(err)
}
after, err := reopened.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
if after != want+1 {
t.Fatalf("generation after reopen mutation: got %d want %d", after, want+1)
}
}
func TestProjectionSnapshotOmitsInactiveRecords(t *testing.T) {
ctx := context.Background()
store, issued, slot, _ := setupRouteTestFixture(t)
route, err := store.CreateRoute(ctx, CreateRouteInput{
PrincipalID: issued.Principal.ID,
SlotID: slot.ID, Alias: "projected-route", ProfileID: "openai", UpstreamModel: "upstream-model",
})
if err != nil {
t.Fatal(err)
}
clock := func() time.Time { return time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) }
snapshot, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{TTL: time.Minute, Clock: clock})
if err != nil {
t.Fatal(err)
}
if len(snapshot.GetTokens()) != 1 || len(snapshot.GetRoutes()) != 1 {
t.Fatalf("active snapshot: tokens=%d routes=%d", len(snapshot.GetTokens()), len(snapshot.GetRoutes()))
}
if snapshot.GetTokens()[0].GetTokenDigestSha256() != issued.Token.Digest {
t.Fatal("snapshot did not contain the active token digest")
}
projectedRoute := snapshot.GetRoutes()[0]
if projectedRoute.GetCredentialSlotRef() != slot.ID || projectedRoute.GetRouteId() != route.ID {
t.Fatalf("route projection mismatch: %+v", projectedRoute)
}
encoded, err := proto.Marshal(snapshot)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encoded), "cipher1") || strings.Contains(string(encoded), "AES-256-GCM") || strings.Contains(string(encoded), "k1") {
t.Fatal("projection contains credential envelope material")
}
if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
t.Fatal(err)
}
currentSlot, err := store.GetSlot(ctx, issued.Principal.ID, slot.ID)
if err != nil {
t.Fatal(err)
}
if _, err := store.DisableSlot(ctx, issued.Principal.ID, slot.ID, currentSlot.Revision); err != nil {
t.Fatal(err)
}
inactiveSnapshot, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{TTL: time.Minute, Clock: clock})
if err != nil {
t.Fatal(err)
}
if len(inactiveSnapshot.GetTokens()) != 0 || len(inactiveSnapshot.GetRoutes()) != 0 {
t.Fatalf("inactive rows leaked: tokens=%d routes=%d", len(inactiveSnapshot.GetTokens()), len(inactiveSnapshot.GetRoutes()))
}
if inactiveSnapshot.GetGeneration() <= snapshot.GetGeneration() {
t.Fatalf("generation did not advance: before=%d after=%d", snapshot.GetGeneration(), inactiveSnapshot.GetGeneration())
}
}
func TestProjectionSnapshotEnforcesBounds(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "bounded-a"}); err != nil {
t.Fatal(err)
}
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "bounded-b"}); err != nil {
t.Fatal(err)
}
_, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{
TTL: time.Minute,
Limits: ProjectionLimits{MaxTokens: 1, MaxRoutes: 1, MaxBytes: 4096},
})
if !errors.Is(err, ErrProjectionTooLarge) {
t.Fatalf("token bound: got %v", err)
}
_, err = store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{
TTL: time.Minute,
Limits: ProjectionLimits{MaxTokens: 2, MaxRoutes: 1, MaxBytes: 1},
})
if !errors.Is(err, ErrProjectionTooLarge) {
t.Fatalf("byte bound: got %v", err)
}
}