package auth import ( "context" "encoding/json" "net/http" "sync/atomic" "testing" internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) const homeUnauthorizedRefreshProvider = "home-unauthorized-refresh" type homeUnauthorizedRefreshDispatcher struct { calls atomic.Int32 } func (*homeUnauthorizedRefreshDispatcher) HeartbeatOK() bool { return true } func (d *homeUnauthorizedRefreshDispatcher) RPopAuth(context.Context, string, string, http.Header, int) ([]byte, error) { d.calls.Add(1) return json.Marshal(homeAuthDispatchResponse{Auth: Auth{ ID: "home-refresh-auth", Provider: homeUnauthorizedRefreshProvider, Status: StatusActive, Attributes: map[string]string{ AttributeAuthKind: AuthKindOAuth, "websockets": "true", }, Metadata: map[string]any{ "access_token": "stale-access-token", }, }}) } func (*homeUnauthorizedRefreshDispatcher) AbortAmbiguousDispatch() {} type homeUnauthorizedRefreshExecutor struct { streamMode string refreshErr error keepStale bool retainSelection bool executeCalls atomic.Int32 countCalls atomic.Int32 streamCalls atomic.Int32 refreshCalls atomic.Int32 } func (*homeUnauthorizedRefreshExecutor) Identifier() string { return homeUnauthorizedRefreshProvider } func (e *homeUnauthorizedRefreshExecutor) Execute(_ context.Context, auth *Auth, _ cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { e.executeCalls.Add(1) if e.retainSelection { if lifecycle, ok := opts.ExecutionLifecycle.(interface{ Retain() }); ok { lifecycle.Retain() } } if authAccessToken(auth) == "stale-access-token" { return cliproxyexecutor.Response{}, &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"} } return cliproxyexecutor.Response{Payload: []byte("ok")}, nil } func (e *homeUnauthorizedRefreshExecutor) ExecuteStream(_ context.Context, auth *Auth, _ cliproxyexecutor.Request, _ cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) { e.streamCalls.Add(1) if authAccessToken(auth) == "stale-access-token" { switch e.streamMode { case "bootstrap": chunks := make(chan cliproxyexecutor.StreamChunk, 1) chunks <- cliproxyexecutor.StreamChunk{Err: &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"}} close(chunks) return &cliproxyexecutor.StreamResult{Chunks: chunks}, nil case "started": chunks := make(chan cliproxyexecutor.StreamChunk, 2) chunks <- cliproxyexecutor.StreamChunk{Payload: []byte("started")} chunks <- cliproxyexecutor.StreamChunk{Err: &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"}} close(chunks) return &cliproxyexecutor.StreamResult{Chunks: chunks}, nil default: return nil, &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"} } } chunks := make(chan cliproxyexecutor.StreamChunk, 1) chunks <- cliproxyexecutor.StreamChunk{Payload: []byte("ok")} close(chunks) return &cliproxyexecutor.StreamResult{Chunks: chunks}, nil } func (e *homeUnauthorizedRefreshExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) { e.refreshCalls.Add(1) if e.refreshErr != nil { return nil, e.refreshErr } updated := auth.Clone() if e.keepStale { return updated, nil } if updated.Metadata == nil { updated.Metadata = make(map[string]any) } updated.Metadata["access_token"] = "fresh-access-token" return updated, nil } func (e *homeUnauthorizedRefreshExecutor) CountTokens(_ context.Context, auth *Auth, _ cliproxyexecutor.Request, _ cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { e.countCalls.Add(1) if authAccessToken(auth) == "stale-access-token" { return cliproxyexecutor.Response{}, &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"} } return cliproxyexecutor.Response{Payload: []byte("ok")}, nil } func (*homeUnauthorizedRefreshExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) { return nil, nil } func newHomeUnauthorizedRefreshManager(dispatcher *homeUnauthorizedRefreshDispatcher, executor *homeUnauthorizedRefreshExecutor) *Manager { manager := NewManager(nil, nil, nil) manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}}) manager.PublishHomeDispatch(dispatcher, executionregistry.New(), 1) manager.RegisterExecutor(executor) return manager } func TestHomeUnauthorizedReturnsOriginalErrorWithoutRefresh(t *testing.T) { for _, test := range []struct { name string run func(*Manager) error }{ { name: "execute", run: func(manager *Manager) error { _, errExecute := manager.Execute(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{}) return errExecute }, }, { name: "count_tokens", run: func(manager *Manager) error { _, errCount := manager.ExecuteCount(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{}) return errCount }, }, } { t.Run(test.name, func(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) errRun := test.run(manager) if errRun == nil || errRun.Error() != "access token expired" || statusCodeFromError(errRun) != http.StatusUnauthorized { t.Fatalf("execution error = %v, want original 401", errRun) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } if test.name == "execute" && executor.executeCalls.Load() != 1 { t.Fatalf("execute calls = %d, want 1", executor.executeCalls.Load()) } if test.name == "count_tokens" && executor.countCalls.Load() != 1 { t.Fatalf("count calls = %d, want 1", executor.countCalls.Load()) } }) } } func TestHomeUnauthorizedDoesNotRefreshRetainedSelection(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{retainSelection: true} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) opts := cliproxyexecutor.Options{Metadata: map[string]any{ cliproxyexecutor.ExecutionSessionMetadataKey: "refresh-session", cliproxyexecutor.PinnedAuthMetadataKey: "home-refresh-auth", }} _, errExecute := manager.Execute(ctx, []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, opts) if errExecute == nil || errExecute.Error() != "access token expired" { t.Fatalf("Execute() error = %v, want original upstream error", errExecute) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } if got := executor.executeCalls.Load(); got != 1 { t.Fatalf("execute calls = %d, want 1", got) } } func TestRefreshHomeSelectionReusesConcurrentNewerToken(t *testing.T) { executor := &homeUnauthorizedRefreshExecutor{} selection := &HomeDispatchSelection{ Auth: &Auth{ID: "home-refresh-auth", Provider: homeUnauthorizedRefreshProvider, Attributes: map[string]string{AttributeAuthKind: AuthKindOAuth}, Metadata: map[string]any{"access_token": "fresh-access-token"}}, Executor: executor, Provider: homeUnauthorizedRefreshProvider, } failed := &Auth{ID: "home-refresh-auth", Provider: homeUnauthorizedRefreshProvider, Attributes: map[string]string{AttributeAuthKind: AuthKindOAuth}, Metadata: map[string]any{"access_token": "stale-access-token"}} manager := NewManager(nil, nil, nil) updated, reused, errRefresh := manager.RefreshHomeSelectionAfterUnauthorized(context.Background(), selection, failed) if errRefresh != nil || !reused || authAccessToken(updated) != "fresh-access-token" { t.Fatalf("RefreshHomeSelectionAfterUnauthorized() = %#v, %v, %v", updated, reused, errRefresh) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0 when selection already has a newer token", got) } } func TestHomeUnauthorizedDoesNotRefreshOrReplay(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{keepStale: true} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) _, errExecute := manager.Execute(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{}) if statusCodeFromError(errExecute) != http.StatusUnauthorized { t.Fatalf("Execute() error = %v, want original 401", errExecute) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } if got := executor.executeCalls.Load(); got != 1 { t.Fatalf("execute calls = %d, want 1", got) } } func TestHomeNoCandidatePreservesOriginalUpstreamError(t *testing.T) { upstreamErr := &Error{HTTPStatus: http.StatusUnauthorized, Message: "access token expired"} noCandidate := &Error{Code: "auth_not_found", HTTPStatus: http.StatusServiceUnavailable, Message: "no auth available"} if !shouldReturnLastErrorOnPickFailure(true, upstreamErr, noCandidate) { t.Fatal("Home no-candidate error would overwrite the original upstream error") } } func TestHomeUnauthorizedIgnoresExecutorRefreshFailure(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{ refreshErr: &Error{HTTPStatus: http.StatusServiceUnavailable, Message: "Home refresh temporarily unavailable"}, } manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) _, errExecute := manager.Execute(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{}) if statusCodeFromError(errExecute) != http.StatusUnauthorized || errExecute.Error() != "access token expired" { t.Fatalf("Execute() error = %v, want original 401", errExecute) } if got := executor.executeCalls.Load(); got != 1 { t.Fatalf("execute calls = %d, want 1", got) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } } func TestHomeUnauthorizedStreamDoesNotRefreshOrReplay(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{keepStale: true} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) _, errStream := manager.ExecuteStream(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{Stream: true}) if statusCodeFromError(errStream) != http.StatusUnauthorized { t.Fatalf("ExecuteStream() error = %v, want original 401", errStream) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } if got := executor.streamCalls.Load(); got != 1 { t.Fatalf("stream calls = %d, want 1", got) } } func TestHomeUnauthorizedStartedStreamDoesNotReplay(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{streamMode: "started"} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) result, errStream := manager.ExecuteStream(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{Stream: true}) if errStream != nil { t.Fatalf("ExecuteStream() error = %v", errStream) } sawPayload := false sawUnauthorized := false for chunk := range result.Chunks { if string(chunk.Payload) == "started" { sawPayload = true } if statusCodeFromError(chunk.Err) == http.StatusUnauthorized { sawUnauthorized = true } } if !sawPayload || !sawUnauthorized { t.Fatalf("stream results = payload %v unauthorized %v, want both", sawPayload, sawUnauthorized) } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0 after stream started", got) } if got := executor.streamCalls.Load(); got != 1 { t.Fatalf("stream calls = %d, want 1", got) } } func TestHomeUnauthorizedStreamReturnsOriginalErrorWithoutRefresh(t *testing.T) { for _, mode := range []string{"synchronous", "bootstrap"} { t.Run(mode, func(t *testing.T) { dispatcher := &homeUnauthorizedRefreshDispatcher{} executor := &homeUnauthorizedRefreshExecutor{streamMode: mode} manager := newHomeUnauthorizedRefreshManager(dispatcher, executor) result, errStream := manager.ExecuteStream(context.Background(), []string{homeUnauthorizedRefreshProvider}, cliproxyexecutor.Request{Model: "model-a"}, cliproxyexecutor.Options{Stream: true}) if mode == "synchronous" { if errStream == nil || errStream.Error() != "access token expired" || statusCodeFromError(errStream) != http.StatusUnauthorized { t.Fatalf("ExecuteStream() error = %v, want original 401", errStream) } } else { if errStream != nil { t.Fatalf("ExecuteStream() error = %v", errStream) } chunk, ok := <-result.Chunks if !ok || chunk.Err == nil || chunk.Err.Error() != "access token expired" || statusCodeFromError(chunk.Err) != http.StatusUnauthorized { t.Fatalf("stream chunk = %#v, open=%v; want original 401", chunk, ok) } } if got := executor.refreshCalls.Load(); got != 0 { t.Fatalf("refresh calls = %d, want 0", got) } if got := executor.streamCalls.Load(); got != 1 { t.Fatalf("stream calls = %d, want 1", got) } }) } }