Files
cpa-plugin/internal/plugin/access_test.go
T

153 lines
5.7 KiB
Go

package plugin
import (
"context"
"encoding/json"
"net/http"
"testing"
"time"
managedaccess "cpa-ext/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)
}
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)
}
store, _ := app.currentStore()
if err := store.SyncUpstreamAccounts(context.Background(), []managedaccess.UpstreamAccount{{
CPAAuthID: "oauth-1", CPAAuthIndex: "index-1", Provider: "codex", DisplayName: "OAuth One", 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", 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 TestCreateKeyCopiesDefaultRuleSnapshot(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":"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 {
t.Fatalf("created = %+v", created)
}
}