feat(vnext2): add user key self-service skeleton
- PORTAL_KEY_EXPERIENCE.md: review from pending to approved - KEY_SELF_SERVICE_API.md: review from pending to approved - 0015_user_keys.sql: migration for key_records table - user_keys_repo.go + test: SQLite repo (Create/ListByOwner/GetByID/UpdateStatus) - key_self_service.go: HTTP handlers (POST/GET /api/keys, pause/resume/delete) - key_self_service_svc.go: action wiring (buildUserKeyHandler) - registered in ActionSet + NewAPIHandlerWithAuth Note: full user auth requires host+CRM co-deployment. Current skeleton accepts Bearer token for testing.
This commit is contained in:
72
internal/store/sqlite/user_keys_repo_test.go
Normal file
72
internal/store/sqlite/user_keys_repo_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserKeysRepoCreateListGet(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create
|
||||
id, err := store.UserKeys().Create(ctx, UserKeyRecord{
|
||||
KeyID: "key_test_001",
|
||||
OwnerSubjectID: "user_long",
|
||||
KeyFingerprint: "sha256:fake_fingerprint",
|
||||
MaskedPreview: "sk-****abcd",
|
||||
DisplayName: "test key",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
AllowedModels: []string{"gpt-5.4"},
|
||||
AdminStatus: "active",
|
||||
QuotaStatus: "ok",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Fatalf("Create() id = %d, want >0", id)
|
||||
}
|
||||
|
||||
// List by owner
|
||||
keys, err := store.UserKeys().ListByOwner(ctx, "user_long")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByOwner() error = %v", err)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("ListByOwner() len = %d, want 1", len(keys))
|
||||
}
|
||||
if keys[0].KeyID != "key_test_001" {
|
||||
t.Fatalf("key_id = %q, want %q", keys[0].KeyID, "key_test_001")
|
||||
}
|
||||
if len(keys[0].AllowedModels) != 1 || keys[0].AllowedModels[0] != "gpt-5.4" {
|
||||
t.Fatalf("AllowedModels = %v, want [gpt-5.4]", keys[0].AllowedModels)
|
||||
}
|
||||
|
||||
// Get by ID
|
||||
key, err := store.UserKeys().GetByID(ctx, "key_test_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID() error = %v", err)
|
||||
}
|
||||
if key.MaskedPreview != "sk-****abcd" {
|
||||
t.Fatalf("MaskedPreview = %q, want %q", key.MaskedPreview, "sk-****abcd")
|
||||
}
|
||||
|
||||
// Update status
|
||||
if err := store.UserKeys().UpdateStatus(ctx, "key_test_001", "paused"); err != nil {
|
||||
t.Fatalf("UpdateStatus() error = %v", err)
|
||||
}
|
||||
key, _ = store.UserKeys().GetByID(ctx, "key_test_001")
|
||||
if key.AdminStatus != "paused" {
|
||||
t.Fatalf("After pause: admin_status = %q, want %q", key.AdminStatus, "paused")
|
||||
}
|
||||
|
||||
// Owner isolation: other user sees nothing
|
||||
otherKeys, err := store.UserKeys().ListByOwner(ctx, "user_other")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByOwner(other) error = %v", err)
|
||||
}
|
||||
if len(otherKeys) != 0 {
|
||||
t.Fatalf("other user keys = %d, want 0", len(otherKeys))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user