Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

544 lines
16 KiB
Go

package managedcredentials
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
managedcredentialsbiz "github.com/tutti-os/tutti/services/tuttid/biz/managedcredentials"
)
func TestServiceGrantCodeIsOneTimeAndGrantRefRefreshes(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
store := newManagedCredentialsMemoryStore()
service := &Service{
Store: store,
Now: func() time.Time {
return now
},
}
apiKey := "agnes-secret"
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
APIKey: &apiKey,
BaseURL: "https://agnes.example/v1",
Models: []managedcredentialsbiz.Model{{
ID: "agnes-2.0-flash",
Name: "Agnes 2.0 Flash",
Provider: managedcredentialsbiz.ProviderAgnes,
}},
}); err != nil {
t.Fatalf("PutProvider: %v", err)
}
grant, err := service.CreateGrant(ctx, CreateGrantInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
Nonce: "nonce-1",
ProviderIDs: []string{"agnes"},
Scopes: []string{"models:use"},
State: "state-1",
})
if err != nil {
t.Fatalf("CreateGrant: %v", err)
}
firstExchange, err := service.Exchange(ctx, ExchangeInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantCode: grant.GrantCode,
Nonce: "nonce-1",
State: "state-1",
})
if err != nil {
t.Fatalf("first Exchange: %v", err)
}
if firstExchange.GrantRef != grant.Grant.GrantRef {
t.Fatalf("first Exchange grantRef = %q, want %q", firstExchange.GrantRef, grant.Grant.GrantRef)
}
if firstExchange.Providers[0] != managedcredentialsbiz.ProviderAgnes {
t.Fatalf("first Exchange provider = %q, want agnes", firstExchange.Providers[0])
}
credential, err := service.Credential(ctx, CredentialInput{
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantRef: firstExchange.GrantRef,
Provider: "agnes",
Model: "agnes-2.0-flash",
Capability: "agent",
})
if err != nil {
t.Fatalf("Credential: %v", err)
}
if got := credential.Credential.APIKey; got != "agnes-secret" {
t.Fatalf("Credential API key = %q", got)
}
if _, err := service.Exchange(ctx, ExchangeInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantCode: grant.GrantCode,
Nonce: "nonce-1",
State: "state-1",
}); !errors.Is(err, ErrGrantCodeInvalid) {
t.Fatalf("second Exchange with same code error = %v, want %v", err, ErrGrantCodeInvalid)
}
// Within the grant lease the grantRef can be reused, but the reported
// expiry stays pinned to the original lease rather than rolling forward.
now = now.Add(1 * time.Hour)
refreshCatalog, err := service.ListGrantModels(ctx,
"workspace-1",
"app-1",
grant.Grant.GrantRef,
)
if err != nil {
t.Fatalf("refresh ListGrantModels: %v", err)
}
if !refreshCatalog.ExpiresAt.Equal(grant.Grant.ExpiresAt) {
t.Fatalf("refresh expiry = %s, want fixed grant expiry %s", refreshCatalog.ExpiresAt, grant.Grant.ExpiresAt)
}
refreshCredential, err := service.Credential(ctx, CredentialInput{
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantRef: grant.Grant.GrantRef,
Provider: "agnes",
Model: "agnes-2.0-flash",
Capability: "agent",
})
if err != nil {
t.Fatalf("refresh Credential: %v", err)
}
if !refreshCredential.ExpiresAt.Equal(grant.Grant.ExpiresAt) {
t.Fatalf("credential expiry = %s, want fixed grant expiry %s", refreshCredential.ExpiresAt, grant.Grant.ExpiresAt)
}
}
func TestServiceRejectsExpiredGrant(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
store := newManagedCredentialsMemoryStore()
service := &Service{
Store: store,
Now: func() time.Time {
return now
},
}
apiKey := "agnes-secret"
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
APIKey: &apiKey,
BaseURL: "https://agnes.example/v1",
Models: []managedcredentialsbiz.Model{{
ID: "agnes-2.0-flash",
Name: "Agnes 2.0 Flash",
Provider: managedcredentialsbiz.ProviderAgnes,
}},
}); err != nil {
t.Fatalf("PutProvider: %v", err)
}
grant, err := service.CreateGrant(ctx, CreateGrantInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
Nonce: "nonce-1",
ProviderIDs: []string{"agnes"},
Scopes: []string{"models:use"},
State: "state-1",
})
if err != nil {
t.Fatalf("CreateGrant: %v", err)
}
// Exchange the one-time code within the lease so we hold a live grantRef.
if _, err := service.Exchange(ctx, ExchangeInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantCode: grant.GrantCode,
Nonce: "nonce-1",
State: "state-1",
}); err != nil {
t.Fatalf("Exchange: %v", err)
}
// Advancing past the lease must stop the grantRef from minting credentials.
now = now.Add(GrantCodeTTL + time.Second)
if _, err := service.Credential(ctx, CredentialInput{
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantRef: grant.Grant.GrantRef,
Provider: "agnes",
Model: "agnes-2.0-flash",
Capability: "agent",
}); !errors.Is(err, ErrGrantExpired) {
t.Fatalf("Credential after expiry error = %v, want %v", err, ErrGrantExpired)
}
if _, err := service.ListGrantModels(ctx, "workspace-1", "app-1", grant.Grant.GrantRef); !errors.Is(err, ErrGrantExpired) {
t.Fatalf("ListGrantModels after expiry error = %v, want %v", err, ErrGrantExpired)
}
}
func TestServiceRejectsExpiredGrantCode(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
store := newManagedCredentialsMemoryStore()
service := &Service{
Store: store,
Now: func() time.Time {
return now
},
}
grant, err := service.CreateGrant(ctx, CreateGrantInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
Nonce: "nonce-1",
ProviderIDs: []string{"agnes"},
State: "state-1",
})
if err != nil {
t.Fatalf("CreateGrant: %v", err)
}
now = now.Add(GrantCodeTTL + time.Second)
_, err = service.Exchange(ctx, ExchangeInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantCode: grant.GrantCode,
Nonce: "nonce-1",
State: "state-1",
})
if !errors.Is(err, ErrGrantCodeInvalid) {
t.Fatalf("Exchange error = %v, want %v", err, ErrGrantCodeInvalid)
}
}
func TestServiceRejectsGrantCodeWithMismatchedChallengeContext(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
service := &Service{Store: store}
grant, err := service.CreateGrant(ctx, CreateGrantInput{
ContextToken: "context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
Nonce: "nonce-1",
ProviderIDs: []string{"agnes"},
State: "state-1",
})
if err != nil {
t.Fatalf("CreateGrant: %v", err)
}
_, err = service.Exchange(ctx, ExchangeInput{
ContextToken: "different-context-token",
WorkspaceID: "workspace-1",
AppID: "app-1",
GrantCode: grant.GrantCode,
Nonce: "nonce-1",
State: "state-1",
})
if !errors.Is(err, ErrGrantCodeInvalid) {
t.Fatalf("Exchange error = %v, want %v", err, ErrGrantCodeInvalid)
}
}
func TestServicePutProviderPreservesOmittedAPIKeyAndClearsBlankAPIKey(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
service := &Service{Store: store}
apiKey := "agnes-secret"
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
APIKey: &apiKey,
BaseURL: "https://agnes.example/v1",
}); err != nil {
t.Fatalf("initial PutProvider: %v", err)
}
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
BaseURL: "https://agnes-alt.example/v1",
}); err != nil {
t.Fatalf("second PutProvider: %v", err)
}
config, err := store.GetManagedModelProviderConfig(ctx, "workspace-1", managedcredentialsbiz.ProviderAgnes)
if err != nil {
t.Fatalf("GetManagedModelProviderConfig: %v", err)
}
if config.APIKey != apiKey {
t.Fatalf("APIKey = %q, want preserved key", config.APIKey)
}
blankAPIKey := " "
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
APIKey: &blankAPIKey,
BaseURL: "https://agnes-empty-key.example/v1",
}); err != nil {
t.Fatalf("blank-key PutProvider: %v", err)
}
config, err = store.GetManagedModelProviderConfig(ctx, "workspace-1", managedcredentialsbiz.ProviderAgnes)
if err != nil {
t.Fatalf("GetManagedModelProviderConfig after blank key: %v", err)
}
if config.APIKey != "" {
t.Fatalf("APIKey after blank key = %q, want cleared key", config.APIKey)
}
}
func TestServiceListProvidersReturnsEmptyModelArrayAndRedactsSavedAPIKey(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
service := &Service{Store: store}
store.providers["workspace-1:agnes"] = managedcredentialsbiz.ProviderConfig{
WorkspaceID: "workspace-1",
Provider: managedcredentialsbiz.ProviderAgnes,
Enabled: true,
APIKey: "agnes-secret",
Models: nil,
}
providers, err := service.ListProviders(ctx, "workspace-1")
if err != nil {
t.Fatalf("ListProviders: %v", err)
}
if len(providers) != 1 {
t.Fatalf("provider count = %d, want 1", len(providers))
}
if providers[0].Models == nil {
t.Fatal("provider Models is nil, want empty slice")
}
if len(providers[0].Models) != 0 {
t.Fatalf("provider Models length = %d, want 0", len(providers[0].Models))
}
if providers[0].APIKey != "" {
t.Fatalf("provider APIKey = %q, want redacted key", providers[0].APIKey)
}
if !providers[0].HasAPIKey {
t.Fatal("provider HasAPIKey = false, want true")
}
}
func TestServiceListProviderModelsFetchesOpenAICompatibleCatalog(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
var gotPath string
var gotAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"data": [
{"id": "agnes-2.0-flash", "name": "Agnes 2.0 Flash"},
{"id": "agnes-2.0-pro"},
{"id": "agnes-2.0-flash"},
{"id": " "}
]
}`))
}))
defer server.Close()
service := &Service{
Store: store,
HTTPClient: server.Client(),
}
apiKey := "agnes-secret"
if _, err := service.PutProvider(ctx, PutProviderInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
Enabled: true,
APIKey: &apiKey,
BaseURL: server.URL + "/v1",
}); err != nil {
t.Fatalf("PutProvider: %v", err)
}
result, err := service.ListProviderModels(ctx, ListProviderModelsInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
})
if err != nil {
t.Fatalf("ListProviderModels: %v", err)
}
if gotPath != "/v1/models" {
t.Fatalf("request path = %q, want /v1/models", gotPath)
}
if gotAuth != "Bearer agnes-secret" {
t.Fatalf("Authorization = %q, want bearer token", gotAuth)
}
if len(result.Models) != 2 {
t.Fatalf("model count = %d, want 2", len(result.Models))
}
if result.Models[0].ID != "agnes-2.0-flash" || result.Models[0].Name != "Agnes 2.0 Flash" {
t.Fatalf("first model = %#v", result.Models[0])
}
if result.Models[1].ID != "agnes-2.0-pro" || result.Models[1].Provider != managedcredentialsbiz.ProviderAgnes {
t.Fatalf("second model = %#v", result.Models[1])
}
}
func TestServiceListProviderModelsUsesOverrideInput(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
var gotPath string
var gotAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"agnes-2.0-pro"}]}`))
}))
defer server.Close()
service := &Service{
Store: store,
HTTPClient: server.Client(),
}
apiKey := "unsaved-secret"
result, err := service.ListProviderModels(ctx, ListProviderModelsInput{
WorkspaceID: "workspace-1",
Provider: "agnes",
APIKey: &apiKey,
BaseURL: server.URL + "/v1",
})
if err != nil {
t.Fatalf("ListProviderModels: %v", err)
}
if gotPath != "/v1/models" {
t.Fatalf("request path = %q, want /v1/models", gotPath)
}
if gotAuth != "Bearer unsaved-secret" {
t.Fatalf("Authorization = %q, want override bearer token", gotAuth)
}
if len(result.Models) != 1 || result.Models[0].ID != "agnes-2.0-pro" {
t.Fatalf("models = %#v", result.Models)
}
}
func TestServiceListProviderModelsTriesVersionedBaseModelsFirst(t *testing.T) {
ctx := context.Background()
store := newManagedCredentialsMemoryStore()
var gotPaths []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPaths = append(gotPaths, r.URL.Path)
if r.URL.Path != "/api/coding/paas/v4/models" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"glm-coding"}]}`))
}))
defer server.Close()
service := &Service{
Store: store,
HTTPClient: server.Client(),
}
apiKey := "coding-secret"
result, err := service.ListProviderModels(ctx, ListProviderModelsInput{
WorkspaceID: "workspace-1",
Provider: "openai",
APIKey: &apiKey,
BaseURL: server.URL + "/api/coding/paas/v4",
})
if err != nil {
t.Fatalf("ListProviderModels: %v", err)
}
if len(gotPaths) != 1 || gotPaths[0] != "/api/coding/paas/v4/models" {
t.Fatalf("request paths = %#v, want first versioned /models", gotPaths)
}
if len(result.Models) != 1 || result.Models[0].ID != "glm-coding" {
t.Fatalf("models = %#v", result.Models)
}
}
type managedCredentialsMemoryStore struct {
grants map[string]managedcredentialsbiz.Grant
providers map[string]managedcredentialsbiz.ProviderConfig
}
func newManagedCredentialsMemoryStore() *managedCredentialsMemoryStore {
return &managedCredentialsMemoryStore{
grants: map[string]managedcredentialsbiz.Grant{},
providers: map[string]managedcredentialsbiz.ProviderConfig{},
}
}
func (*managedCredentialsMemoryStore) DeleteManagedModelGrant(context.Context, string, string, string) error {
return nil
}
func (s *managedCredentialsMemoryStore) DeleteManagedModelProviderConfig(_ context.Context, workspaceID string, provider managedcredentialsbiz.ProviderID) error {
delete(s.providers, workspaceID+":"+string(provider))
return nil
}
func (s *managedCredentialsMemoryStore) GetManagedModelGrant(_ context.Context, workspaceID string, appID string, grantRef string) (managedcredentialsbiz.Grant, error) {
grant, ok := s.grants[workspaceID+":"+appID+":"+grantRef]
if !ok {
return managedcredentialsbiz.Grant{}, errors.New("grant not found")
}
return grant, nil
}
func (s *managedCredentialsMemoryStore) GetManagedModelProviderConfig(_ context.Context, workspaceID string, provider managedcredentialsbiz.ProviderID) (managedcredentialsbiz.ProviderConfig, error) {
config, ok := s.providers[workspaceID+":"+string(provider)]
if !ok {
return managedcredentialsbiz.ProviderConfig{}, ErrProviderNotConfigured
}
return config, nil
}
func (s *managedCredentialsMemoryStore) ListManagedModelProviderConfigs(_ context.Context, workspaceID string) ([]managedcredentialsbiz.ProviderConfig, error) {
var configs []managedcredentialsbiz.ProviderConfig
for _, config := range s.providers {
if config.WorkspaceID == workspaceID {
configs = append(configs, config)
}
}
return configs, nil
}
func (s *managedCredentialsMemoryStore) PutManagedModelGrant(_ context.Context, grant managedcredentialsbiz.Grant) error {
s.grants[grant.WorkspaceID+":"+grant.AppID+":"+grant.GrantRef] = grant
return nil
}
func (s *managedCredentialsMemoryStore) PutManagedModelProviderConfig(_ context.Context, config managedcredentialsbiz.ProviderConfig) error {
s.providers[config.WorkspaceID+":"+string(config.Provider)] = config
return nil
}
func (s *managedCredentialsMemoryStore) RevokeManagedModelGrant(_ context.Context, workspaceID string, appID string, grantRef string) error {
grant, ok := s.grants[workspaceID+":"+appID+":"+grantRef]
if !ok {
return errors.New("grant not found")
}
now := time.Now().UTC()
grant.RevokedAt = &now
s.grants[workspaceID+":"+appID+":"+grantRef] = grant
return nil
}