vibe-proxy/backend/sdk/auth/filestore_test.go
2026-08-24 00:10:41 +02:00

403 lines
12 KiB
Go

package auth
import (
"context"
"os"
"path/filepath"
"testing"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
func TestExtractAccessToken(t *testing.T) {
t.Parallel()
tests := []struct {
name string
metadata map[string]any
expected string
}{
{
"antigravity top-level access_token",
map[string]any{"access_token": "tok-abc"},
"tok-abc",
},
{
"gemini nested token.access_token",
map[string]any{
"token": map[string]any{"access_token": "tok-nested"},
},
"tok-nested",
},
{
"top-level takes precedence over nested",
map[string]any{
"access_token": "tok-top",
"token": map[string]any{"access_token": "tok-nested"},
},
"tok-top",
},
{
"empty metadata",
map[string]any{},
"",
},
{
"whitespace-only access_token",
map[string]any{"access_token": " "},
"",
},
{
"wrong type access_token",
map[string]any{"access_token": 12345},
"",
},
{
"token is not a map",
map[string]any{"token": "not-a-map"},
"",
},
{
"nested whitespace-only",
map[string]any{
"token": map[string]any{"access_token": " "},
},
"",
},
{
"fallback to nested when top-level empty",
map[string]any{
"access_token": "",
"token": map[string]any{"access_token": "tok-fallback"},
},
"tok-fallback",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := extractAccessToken(tt.metadata)
if got != tt.expected {
t.Errorf("extractAccessToken() = %q, want %q", got, tt.expected)
}
})
}
}
func TestFileTokenStoreSaveExistingMetadataSetsFileAttributes(t *testing.T) {
tests := []struct {
name string
existingToken string
savedToken string
}{
{name: "unchanged content", existingToken: "token", savedToken: "token"},
{name: "overwritten content", existingToken: "old-token", savedToken: "new-token"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
baseDir := t.TempDir()
fileName := "antigravity-user.json"
path := filepath.Join(baseDir, fileName)
existing := []byte(`{"type":"antigravity","access_token":"` + tt.existingToken + `","disabled":false}`)
if errWrite := os.WriteFile(path, existing, 0o600); errWrite != nil {
t.Fatalf("write existing auth file: %v", errWrite)
}
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auth := &cliproxyauth.Auth{
ID: fileName,
FileName: fileName,
Metadata: map[string]any{
"type": "antigravity",
"access_token": tt.savedToken,
},
}
savedPath, errSave := store.Save(context.Background(), auth)
if errSave != nil {
t.Fatalf("Save() error = %v", errSave)
}
if savedPath != path {
t.Fatalf("Save() path = %q, want %q", savedPath, path)
}
if got := auth.Attributes[cliproxyauth.AttributePath]; got != path {
t.Errorf("path attribute = %q, want %q", got, path)
}
if got := auth.Attributes[cliproxyauth.AttributeSource]; got != path {
t.Errorf("source attribute = %q, want %q", got, path)
}
if got := auth.Attributes[cliproxyauth.AttributeSourceBackend]; got != cliproxyauth.AuthSourceFile {
t.Errorf("source backend attribute = %q, want %q", got, cliproxyauth.AuthSourceFile)
}
persisted, errRead := os.ReadFile(path)
if errRead != nil {
t.Fatalf("read saved auth file: %v", errRead)
}
expected := []byte(`{"type":"antigravity","access_token":"` + tt.savedToken + `","disabled":false}`)
if !jsonEqual(persisted, expected) {
t.Errorf("saved auth file = %s, want JSON equal to %s", persisted, expected)
}
})
}
}
func TestFileTokenStoreNormalizesLegacyCredentialMetadata(t *testing.T) {
t.Run("save", func(t *testing.T) {
baseDir := t.TempDir()
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auth := &cliproxyauth.Auth{
ID: "legacy-save.json",
FileName: "legacy-save.json",
Metadata: map[string]any{
"type": "codex",
"request-retry": 2,
"request_retry": 0,
"disable-cooling": true,
},
}
path, errSave := store.Save(context.Background(), auth)
if errSave != nil {
t.Fatalf("Save() error = %v", errSave)
}
persisted, errRead := os.ReadFile(path)
if errRead != nil {
t.Fatalf("read saved auth file: %v", errRead)
}
want := []byte(`{"type":"codex","request_retry":0,"disable_cooling":true,"disabled":false}`)
if !jsonEqual(persisted, want) {
t.Fatalf("saved auth file = %s, want JSON equal to %s", persisted, want)
}
})
t.Run("list", func(t *testing.T) {
baseDir := t.TempDir()
path := filepath.Join(baseDir, "legacy-list.json")
if errWrite := os.WriteFile(path, []byte(`{"type":"codex","request-retry":2,"disable-cooling":true}`), 0o600); errWrite != nil {
t.Fatalf("write legacy auth file: %v", errWrite)
}
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auths, errList := store.List(context.Background())
if errList != nil {
t.Fatalf("List() error = %v", errList)
}
if len(auths) != 1 {
t.Fatalf("List() len = %d, want 1", len(auths))
}
if got := auths[0].Metadata["request_retry"]; got != float64(2) {
t.Fatalf("listed request_retry = %#v, want 2", got)
}
if got := auths[0].Metadata["disable_cooling"]; got != true {
t.Fatalf("listed disable_cooling = %#v, want true", got)
}
for _, legacy := range []string{"request-retry", "disable-cooling"} {
if _, exists := auths[0].Metadata[legacy]; exists {
t.Fatalf("listed metadata retained %q: %#v", legacy, auths[0].Metadata)
}
}
})
}
func TestFileTokenStoreSaveRejectsInvalidWeight(t *testing.T) {
baseDir := t.TempDir()
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auth := &cliproxyauth.Auth{
ID: "invalid.json",
FileName: "invalid.json",
Metadata: map[string]any{
"type": "test",
cliproxyauth.AttributeWeight: 1.5,
},
}
if _, errSave := store.Save(context.Background(), auth); errSave == nil {
t.Fatal("Save() accepted an invalid weight")
}
if _, errStat := os.Stat(filepath.Join(baseDir, auth.FileName)); !os.IsNotExist(errStat) {
t.Fatalf("invalid auth file was persisted: %v", errStat)
}
}
func TestFileTokenStoreListSkipsInvalidPluginSourceWeight(t *testing.T) {
baseDir := t.TempDir()
path := filepath.Join(baseDir, "plugin.json")
if errWrite := os.WriteFile(path, []byte(`{"type":"plugin","weight":"invalid"}`), 0o600); errWrite != nil {
t.Fatalf("write auth file: %v", errWrite)
}
parserCalled := false
RegisterPluginAuthParser(fileStoreMultiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error) {
parserCalled = true
return []*cliproxyauth.Auth{{ID: "plugin.json", Provider: "plugin"}}, true, nil
}))
t.Cleanup(func() {
RegisterPluginAuthParser(nil)
})
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auths, errList := store.List(context.Background())
if errList != nil {
t.Fatalf("List() error = %v", errList)
}
if parserCalled {
t.Fatal("plugin parser was called for an invalid persisted source")
}
if len(auths) != 0 {
t.Fatalf("List() returned invalid plugin auths: %#v", auths)
}
}
func TestFileTokenStoreListExpandsPluginMultiAuths(t *testing.T) {
baseDir := t.TempDir()
path := filepath.Join(baseDir, "geminicli.json")
if errWrite := os.WriteFile(path, []byte(`{"type":"gemini-cli","weight":3,"headers":{"X-Test":"value"}}`), 0o600); errWrite != nil {
t.Fatalf("write auth file: %v", errWrite)
}
RegisterPluginAuthParser(fileStoreMultiAuthParserFunc(func(ctx context.Context, req pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error) {
if req.Provider != "gemini-cli" || req.Path != path || req.FileName != "geminicli.json" {
t.Fatalf("ParseAuths request = %#v, want file context", req)
}
return []*cliproxyauth.Auth{
{
ID: "geminicli.json",
Provider: "gemini-cli",
Metadata: map[string]any{
"type": "gemini-cli",
"headers": map[string]any{
"X-Test": "value",
},
},
},
nil,
{
ID: "geminicli-project-a.json",
Provider: "gemini-cli",
Metadata: map[string]any{
"type": "gemini-cli",
"project_id": "project-a",
"headers": map[string]any{
"X-Test": "value",
},
},
},
}, true, nil
}))
t.Cleanup(func() {
RegisterPluginAuthParser(nil)
})
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auths, errList := store.List(context.Background())
if errList != nil {
t.Fatalf("List() error = %v", errList)
}
if len(auths) != 2 {
t.Fatalf("List() len = %d, want two plugin auths", len(auths))
}
if firstIndex, secondIndex := auths[0].EnsureIndex(), auths[1].EnsureIndex(); firstIndex == "" || firstIndex == secondIndex {
t.Fatalf("auth indexes = %q/%q, want distinct non-empty indexes", firstIndex, secondIndex)
}
for _, auth := range auths {
if !cliproxyauth.IsPluginVirtualAuth(auth) {
t.Fatalf("auth attributes = %#v, want plugin virtual marker", auth.Attributes)
}
if auth.Attributes[cliproxyauth.AttributeVirtualSource] != path {
t.Fatalf("virtual_source = %q, want %q", auth.Attributes[cliproxyauth.AttributeVirtualSource], path)
}
if auth.Attributes["path"] != path || auth.Attributes["source"] != path {
t.Fatalf("auth attributes = %#v, want source path", auth.Attributes)
}
if gotHeader := auth.Attributes["header:X-Test"]; gotHeader != "value" {
t.Fatalf("header:X-Test = %q, want value", gotHeader)
}
if gotWeight := auth.Attributes[cliproxyauth.AttributeWeight]; gotWeight != "3" {
t.Fatalf("weight = %q, want 3", gotWeight)
}
}
if gotProject := auths[1].Metadata["project_id"]; gotProject != "project-a" {
t.Fatalf("project_id = %#v, want project-a", gotProject)
}
}
func TestFileTokenStoreListAppliesSourceDisabledToPluginMultiAuths(t *testing.T) {
baseDir := t.TempDir()
path := filepath.Join(baseDir, "geminicli.json")
if errWrite := os.WriteFile(path, []byte(`{"type":"gemini-cli","disabled":true}`), 0o600); errWrite != nil {
t.Fatalf("write auth file: %v", errWrite)
}
RegisterPluginAuthParser(fileStoreMultiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error) {
return []*cliproxyauth.Auth{
{ID: "geminicli.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli"}},
{ID: "geminicli-project-a.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli", "project_id": "project-a"}},
}, true, nil
}))
t.Cleanup(func() {
RegisterPluginAuthParser(nil)
})
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auths, errList := store.List(context.Background())
if errList != nil {
t.Fatalf("List() error = %v", errList)
}
if len(auths) != 2 {
t.Fatalf("List() len = %d, want two plugin auths", len(auths))
}
for _, auth := range auths {
if !auth.Disabled || auth.Status != cliproxyauth.StatusDisabled {
t.Fatalf("auth %s disabled/status = %v/%s, want disabled", auth.ID, auth.Disabled, auth.Status)
}
if got, _ := auth.Metadata["disabled"].(bool); !got {
t.Fatalf("auth %s metadata disabled = %#v, want true", auth.ID, auth.Metadata["disabled"])
}
}
}
func TestFileTokenStoreListPluginHandledEmptySuppressesBuiltin(t *testing.T) {
baseDir := t.TempDir()
path := filepath.Join(baseDir, "codex.json")
if errWrite := os.WriteFile(path, []byte(`{"type":"codex","access_token":"token"}`), 0o600); errWrite != nil {
t.Fatalf("write auth file: %v", errWrite)
}
RegisterPluginAuthParser(fileStoreMultiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error) {
return nil, true, nil
}))
t.Cleanup(func() {
RegisterPluginAuthParser(nil)
})
store := NewFileTokenStore()
store.SetBaseDir(baseDir)
auths, errList := store.List(context.Background())
if errList != nil {
t.Fatalf("List() error = %v", errList)
}
if len(auths) != 0 {
t.Fatalf("List() len = %d, want plugin-handled empty result", len(auths))
}
}
type fileStoreMultiAuthParserFunc func(context.Context, pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error)
func (f fileStoreMultiAuthParserFunc) ParseAuth(context.Context, pluginapi.AuthParseRequest) (*cliproxyauth.Auth, bool, error) {
return nil, false, nil
}
func (f fileStoreMultiAuthParserFunc) ParseAuths(ctx context.Context, req pluginapi.AuthParseRequest) ([]*cliproxyauth.Auth, bool, error) {
return f(ctx, req)
}