305 lines
14 KiB
Go
305 lines
14 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
managedaccess "billing/internal/access"
|
|
)
|
|
|
|
func TestManagedKeyAuthenticationAndLifecycle(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
auth := func(secret string) FrontendAuthResponse {
|
|
raw, _ := json.Marshal(FrontendAuthRequest{Headers: http.Header{"Authorization": []string{"Bearer " + secret}}})
|
|
response, err := app.HandleMethod(MethodFrontendAuthenticate, raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope Envelope
|
|
_ = json.Unmarshal(response, &envelope)
|
|
var result FrontendAuthResponse
|
|
_ = json.Unmarshal(envelope.Result, &result)
|
|
return result
|
|
}
|
|
if got := auth("000000"); !got.Authenticated || got.Principal != "key_default" {
|
|
t.Fatalf("default auth = %+v", got)
|
|
}
|
|
if got := auth("key_default"); got.Authenticated {
|
|
t.Fatalf("internal Key ID authenticated as a credential: %+v", got)
|
|
}
|
|
xAPIKeyRaw, _ := json.Marshal(FrontendAuthRequest{Headers: http.Header{"X-Api-Key": []string{"000000"}}})
|
|
xAPIKeyResponse, _ := app.HandleMethod(MethodFrontendAuthenticate, xAPIKeyRaw)
|
|
var xAPIKeyEnvelope Envelope
|
|
_ = json.Unmarshal(xAPIKeyResponse, &xAPIKeyEnvelope)
|
|
var xAPIKeyResult FrontendAuthResponse
|
|
_ = json.Unmarshal(xAPIKeyEnvelope.Result, &xAPIKeyResult)
|
|
if !xAPIKeyResult.Authenticated {
|
|
t.Fatal("x-api-key did not authenticate")
|
|
}
|
|
store, _ := app.currentStore()
|
|
key, _ := store.ManagedKeyByID(context.Background(), "key_default")
|
|
key.Status = managedaccess.StatusDisabled
|
|
if err := store.UpdateManagedKey(context.Background(), key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := auth("000000"); got.Authenticated {
|
|
t.Fatalf("disabled key authenticated: %+v", got)
|
|
}
|
|
key.Status = managedaccess.StatusActive
|
|
if err := store.UpdateManagedKey(context.Background(), key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := auth("000000"); !got.Authenticated {
|
|
t.Fatalf("restored key did not authenticate: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestModelAllowlistAndStrictUpstreamRouting(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
price := []byte(`{"model":"gpt-5.6-sol","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"},"fast_pricing_enabled":false,"fast_multiplier":"2.5"}`)
|
|
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, price); response.StatusCode != http.StatusOK {
|
|
t.Fatalf("put price=%d %s", response.StatusCode, response.Body)
|
|
}
|
|
store, _ := app.currentStore()
|
|
if err := store.SyncUpstreamAccounts(context.Background(), []managedaccess.UpstreamAccount{{
|
|
CPAAuthID: "oauth-1", CPAAuthIndex: "index-1", Provider: "codex", DisplayName: "OAuth One",
|
|
Disabled: true, Unavailable: true, LastSeenAt: time.Now(),
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
accounts, _ := store.ListUpstreamAccounts(context.Background())
|
|
key, _ := store.ManagedKeyByID(context.Background(), "key_default")
|
|
key.RouteMode = managedaccess.RouteStrict
|
|
key.UpstreamAccountID = accounts[0].ID
|
|
key.AllModels = false
|
|
key.Models = []string{"gpt-5.6-sol"}
|
|
if err := store.UpdateManagedKey(context.Background(), key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
interceptRaw, _ := json.Marshal(RequestInterceptRequest{
|
|
RequestID: "req-1", RequestedModel: "other-model",
|
|
Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)},
|
|
})
|
|
response, err := app.HandleMethod(MethodRequestBefore, interceptRaw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope Envelope
|
|
_ = json.Unmarshal(response, &envelope)
|
|
var intercept RequestInterceptResponse
|
|
_ = json.Unmarshal(envelope.Result, &intercept)
|
|
if !intercept.Terminate || intercept.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("model denial = %+v", intercept)
|
|
}
|
|
|
|
pickRaw, _ := json.Marshal(SchedulerPickRequest{
|
|
Model: "gpt-5.6-sol", Options: SchedulerOptions{Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)}},
|
|
Candidates: []SchedulerAuthCandidate{{ID: "oauth-1", Provider: "codex"}},
|
|
})
|
|
response, err = app.HandleMethod(MethodSchedulerPick, pickRaw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = json.Unmarshal(response, &envelope)
|
|
var pick SchedulerPickResponse
|
|
_ = json.Unmarshal(envelope.Result, &pick)
|
|
if !pick.Handled || pick.AuthID != "oauth-1" {
|
|
t.Fatalf("scheduler pick = %+v", pick)
|
|
}
|
|
afterRaw, _ := json.Marshal(RequestInterceptRequest{
|
|
RequestID: "req-1", Model: "gpt-5.6-sol", RequestedModel: "gpt-5.6-sol",
|
|
Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID), selectedAuthMetadata: "oauth-1"},
|
|
})
|
|
afterResponse, _ := app.HandleMethod(MethodRequestAfter, afterRaw)
|
|
_ = json.Unmarshal(afterResponse, &envelope)
|
|
_ = json.Unmarshal(envelope.Result, &intercept)
|
|
if intercept.Terminate {
|
|
t.Fatalf("selected strict target was rejected: %+v", intercept)
|
|
}
|
|
|
|
pickRaw, _ = json.Marshal(SchedulerPickRequest{
|
|
Model: "gpt-5.6-sol", Options: SchedulerOptions{Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)}},
|
|
Candidates: []SchedulerAuthCandidate{{ID: "oauth-2", Provider: "codex"}},
|
|
})
|
|
response, err = app.HandleMethod(MethodSchedulerPick, pickRaw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = json.Unmarshal(response, &envelope)
|
|
if envelope.OK || envelope.Error == nil || envelope.Error.Code != "bound_upstream_unavailable" {
|
|
t.Fatalf("missing strict target did not fail closed: %s", response)
|
|
}
|
|
}
|
|
|
|
func TestModelAllowlistSupportsFullNameWildcards(t *testing.T) {
|
|
key := managedaccess.ManagedKey{AllModels: false, Models: []string{"deepseek-*", "gpt-5.6-sol"}}
|
|
for _, model := range []string{"deepseek-v4-flash", "DEEPSEEK-SOURCE-B", "gpt-5.6-sol"} {
|
|
if !managedKeyAllowsModel(key, model) {
|
|
t.Fatalf("expected %q to match", model)
|
|
}
|
|
}
|
|
for _, model := range []string{"x-deepseek-v4-flash", "gpt-5.6-terra", ""} {
|
|
if managedKeyAllowsModel(key, model) {
|
|
t.Fatalf("expected %q to be denied", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyCopiesDefaultRuleSnapshot(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store, _ := app.currentStore()
|
|
template, err := store.ManagedKeyByID(context.Background(), "key_default")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
template.Name = "renamed-default"
|
|
template.AllModels = false
|
|
template.Models = []string{"deepseek-*"}
|
|
if err := store.UpdateManagedKey(context.Background(), template); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
response := managementCallBody(t, app, http.MethodPost, managementBase+routeKeys, []byte(`{"name":"alice","secret":"alice-000000"}`))
|
|
if response.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create status=%d body=%s", response.StatusCode, response.Body)
|
|
}
|
|
var created managedaccess.ManagedKey
|
|
if err := json.Unmarshal(response.Body, &created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created.Secret != "alice-000000" || created.RouteMode != managedaccess.RouteAuto || created.AllModels || len(created.Models) != 1 || created.Models[0] != "deepseek-*" {
|
|
t.Fatalf("created = %+v", created)
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyRejectsShortSecret(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := managementCallBody(t, app, http.MethodPost, managementBase+routeKeys, []byte(`{"name":"short","secret":"12345"}`))
|
|
if response.StatusCode != http.StatusBadRequest || !strings.Contains(string(response.Body), "6-256") {
|
|
t.Fatalf("short Key response=%d body=%s", response.StatusCode, response.Body)
|
|
}
|
|
}
|
|
|
|
func TestManagedSecretValidation(t *testing.T) {
|
|
for secret, want := range map[string]bool{
|
|
"12345": false, "000000": true, "alice-000000": true, "with space": false, strings.Repeat("x", 257): false,
|
|
} {
|
|
if got := validManagedSecret(secret); got != want {
|
|
t.Fatalf("validManagedSecret(%q)=%v, want %v", secret, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBillingAdmissionConcurrencyAndUsageSettlement(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
price := []byte(`{"model":"deepseek-v4-flash","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"},"fast_pricing_enabled":false,"fast_multiplier":"2.5"}`)
|
|
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, price); response.StatusCode != http.StatusOK {
|
|
t.Fatalf("put price=%d %s", response.StatusCode, response.Body)
|
|
}
|
|
key, _ := app.store.ManagedKeyByID(context.Background(), "key_default")
|
|
key.AllModels = false
|
|
key.Models = []string{"deepseek-*"}
|
|
if err := app.store.UpdateManagedKey(context.Background(), key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
intercept := func(requestID string) RequestInterceptResponse {
|
|
raw, _ := json.Marshal(RequestInterceptRequest{RequestID: requestID, Model: "deepseek-v4-flash", RequestedModel: "deepseek-source-a", Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)}})
|
|
response, err := app.HandleMethod(MethodRequestBefore, raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope Envelope
|
|
_ = json.Unmarshal(response, &envelope)
|
|
var result RequestInterceptResponse
|
|
_ = json.Unmarshal(envelope.Result, &result)
|
|
return result
|
|
}
|
|
if denied := intercept("zero"); !denied.Terminate || denied.StatusCode != http.StatusTooManyRequests || !strings.Contains(string(denied.ResponseBody), "billing_quota_exhausted") {
|
|
t.Fatalf("zero quota response=%+v body=%s", denied, denied.ResponseBody)
|
|
}
|
|
patch := []byte(`{"id":"key_default","billing":{"quota_usd":"1","reset_period":"none","max_concurrency":1}}`)
|
|
if response := managementCallBody(t, app, http.MethodPatch, managementBase+routeKeys, patch); response.StatusCode != http.StatusOK {
|
|
t.Fatalf("update billing=%d %s", response.StatusCode, response.Body)
|
|
}
|
|
if allowed := intercept("one"); allowed.Terminate {
|
|
t.Fatalf("funded request denied: %+v", allowed)
|
|
}
|
|
afterRaw, _ := json.Marshal(RequestInterceptRequest{RequestID: "one", Model: "unpriced-resolved-model", RequestedModel: "deepseek-source-a", Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)}})
|
|
afterResponse, _ := app.HandleMethod(MethodRequestAfter, afterRaw)
|
|
var afterEnvelope Envelope
|
|
_ = json.Unmarshal(afterResponse, &afterEnvelope)
|
|
var afterResult RequestInterceptResponse
|
|
_ = json.Unmarshal(afterEnvelope.Result, &afterResult)
|
|
if !afterResult.Terminate || afterResult.StatusCode != http.StatusServiceUnavailable || !strings.Contains(string(afterResult.ResponseBody), "billing_price_unavailable") {
|
|
t.Fatalf("missing price response=%+v body=%s", afterResult, afterResult.ResponseBody)
|
|
}
|
|
if denied := intercept("two"); !denied.Terminate || !strings.Contains(string(denied.ResponseBody), "billing_concurrency_exceeded") {
|
|
t.Fatalf("concurrency response=%+v body=%s", denied, denied.ResponseBody)
|
|
}
|
|
completionRaw, _ := json.Marshal(RequestCompletion{RequestID: "one", StartedAt: time.Now().Add(-time.Second), CompletedAt: time.Now(), Outcome: RequestCompletionSucceeded, Metadata: map[string]any{callerScopeMetadata: managedaccess.CallerScope(key.ID)}})
|
|
if _, err := app.HandleMethod(MethodRequestComplete, completionRaw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
usage := UsageRecord{APIKey: "000000", Model: "deepseek-v4-flash", RequestedAt: time.Now(), Detail: UsageDetail{OutputTokens: 10_000, TotalTokens: 10_000}}
|
|
raw, _ := json.Marshal(usage)
|
|
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err := app.store.BillingState(context.Background(), key.ID, time.Now())
|
|
if err != nil || state.SpentMicros != 150_000 || state.BalanceMicros != 850_000 || state.ActiveRequests != 0 {
|
|
t.Fatalf("settled state=%+v err=%v", state, err)
|
|
}
|
|
}
|
|
|
|
func TestUsageSettlementResolvesCurrentCPAKeyPrincipal(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
price := []byte(`{"model":"deepseek-v4-flash","base":{"input_per_1m":"1","cache_read_per_1m":"1","cache_write_per_1m":"1","output_per_1m":"2"},"fast_pricing_enabled":false,"fast_multiplier":"2.5"}`)
|
|
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, price); response.StatusCode != http.StatusOK {
|
|
t.Fatalf("put price=%d %s", response.StatusCode, response.Body)
|
|
}
|
|
if _, err := app.store.UpdateBilling(context.Background(), "key_default", managedaccess.BillingSettings{QuotaMicros: 1_000_000, ResetPeriod: managedaccess.ResetNone, MaxConcurrency: 4}, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, reference := range []string{"key_default", managedaccess.CallerScope("key_default"), "000000"} {
|
|
key, err := app.resolveUsageManagedKey(app.store, reference)
|
|
if err != nil || key.ID != "key_default" {
|
|
t.Fatalf("resolve usage reference %q: key=%+v err=%v", reference, key, err)
|
|
}
|
|
}
|
|
usage := UsageRecord{APIKey: "key_default", Model: "deepseek-v4-flash", RequestedAt: time.Now(), Detail: UsageDetail{InputTokens: 10_000, OutputTokens: 10_000, TotalTokens: 20_000}}
|
|
raw, _ := json.Marshal(usage)
|
|
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err := app.store.BillingState(context.Background(), "key_default", time.Now())
|
|
if err != nil || state.SpentMicros != 30_000 || state.BalanceMicros != 970_000 {
|
|
t.Fatalf("principal-settled state=%+v err=%v", state, err)
|
|
}
|
|
}
|