366 lines · 15.7 KB · Go
RawDownload
package helps import (	"bytes"	"context"	"encoding/json"	"errors"	"net/http"	"strings"	"sync/atomic"	"testing" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth") func TestStatusFromHomeErrorCodeMapsAuthenticationErrorToUnauthorized(t *testing.T) {	if got := statusFromHomeErrorCode("authentication_error"); got != http.StatusUnauthorized {		t.Fatalf("statusFromHomeErrorCode(authentication_error) = %d, want %d", got, http.StatusUnauthorized)	}	if got := statusFromHomeErrorCode("unauthorized"); got != http.StatusUnauthorized {		t.Fatalf("statusFromHomeErrorCode(unauthorized) = %d, want %d", got, http.StatusUnauthorized)	}	for _, code := range []string{"auth_not_found", "auth_unavailable", "refresh_temporarily_unavailable", "refresh_unsupported"} {		if got := statusFromHomeErrorCode(code); got != http.StatusServiceUnavailable {			t.Fatalf("statusFromHomeErrorCode(%s) = %d, want %d", code, got, http.StatusServiceUnavailable)		}	}} type fakeHomeRefreshClient struct {	calls           atomic.Int32	authIndex       string	accessTokenHash string	raw             []byte	err             error} func (c *fakeHomeRefreshClient) HeartbeatOK() bool {	return true} func (c *fakeHomeRefreshClient) GetRefreshAuth(_ context.Context, authIndex string, accessTokenHash string) ([]byte, error) {	c.calls.Add(1)	c.authIndex = authIndex	c.accessTokenHash = accessTokenHash	return c.raw, c.err} func TestRefreshAuthViaHomePreservesContextErrors(t *testing.T) {	client := &fakeHomeRefreshClient{err: context.DeadlineExceeded}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient { return client }	t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "codex"}	_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)	if !handled || !errors.Is(errRefresh, context.DeadlineExceeded) {		t.Fatalf("RefreshAuthViaHome() = handled %v err %v, want true/context.DeadlineExceeded", handled, errRefresh)	}} func TestHomeStatusErrLogDiagnosticSanitizesUpstreamFallback(t *testing.T) {	errRefresh := homeStatusErr{		code:     http.StatusBadGateway,		msg:      "upstream EOF access_token=provider-secret",		upstream: true,	}	diagnostic := errRefresh.LogDiagnostic()	if diagnostic != "Home refresh upstream response: status=502" || strings.Contains(diagnostic, "provider-secret") {		t.Fatalf("LogDiagnostic() = %q, want safe upstream fallback", diagnostic)	}	if errRefresh.Error() != "upstream EOF access_token=provider-secret" {		t.Fatalf("Error() = %q, want exact upstream response", errRefresh.Error())	}} func TestRefreshAuthViaHomeMapsTransportFailureToGeneric503(t *testing.T) {	client := &fakeHomeRefreshClient{err: errors.New("dial failed with provider-secret")}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient { return client }	t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "codex"}	_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)	statusErr, okStatus := errRefresh.(interface{ StatusCode() int })	if !handled || !okStatus || statusErr.StatusCode() != http.StatusServiceUnavailable {		t.Fatalf("RefreshAuthViaHome() = handled %v err %v, want generic 503", handled, errRefresh)	}	if strings.Contains(errRefresh.Error(), "provider-secret") {		t.Fatalf("refresh error included transport detail: %v", errRefresh)	}	direct, okDirect := errRefresh.(interface {		DirectResponse() bool		ResponseBody() []byte	})	if !okDirect || direct.DirectResponse() {		t.Fatalf("transport error direct response = %v/%v, want false", okDirect, direct)	}	diagnosticErr, okDiagnostic := errRefresh.(interface{ LogDiagnostic() string })	if !okDiagnostic || !strings.Contains(diagnosticErr.LogDiagnostic(), "dial_failed") {		t.Fatalf("transport log diagnostic = %T/%v, want allowlisted transport cause", errRefresh, errRefresh)	}	if strings.Contains(diagnosticErr.LogDiagnostic(), "provider-secret") {		t.Fatalf("transport log diagnostic leaked provider detail: %q", diagnosticErr.LogDiagnostic())	}} func TestRefreshAuthViaHomeUsesGenericMessageForLegacyErrorEnvelope(t *testing.T) {	client := &fakeHomeRefreshClient{raw: []byte(`{"error":{"type":"error","message":"provider response: refresh_token=provider-secret"}}`)}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient { return client }	t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "codex"}	_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)	statusErr, okStatus := errRefresh.(interface{ StatusCode() int })	if !handled || !okStatus || statusErr.StatusCode() != http.StatusServiceUnavailable {		t.Fatalf("RefreshAuthViaHome() = handled %v err %v, want generic 503", handled, errRefresh)	}	if strings.Contains(errRefresh.Error(), "provider-secret") {		t.Fatalf("refresh error included legacy Home detail: %v", errRefresh)	}	if diagnosticErr, ok := errRefresh.(interface{ LogDiagnostic() string }); !ok || diagnosticErr.LogDiagnostic() != "Home refresh failed: type=error" {		t.Fatalf("legacy Home error type log diagnostic = %T/%v", errRefresh, errRefresh)	}} func TestRefreshAuthViaHomeUsesDedicatedDiagnosticOnlyForLogs(t *testing.T) {	const diagnostic = "antigravity refresh failed: stage=transport err=EOF"	client := &fakeHomeRefreshClient{raw: []byte(`{"error":{"type":"refresh_temporarily_unavailable","message":"untrusted provider-secret","diagnostic":"` + diagnostic + `"}}`)}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient { return client }	t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "antigravity"}	_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)	if !handled || errRefresh == nil || errRefresh.Error() != "credential refresh temporarily unavailable" {		t.Fatalf("RefreshAuthViaHome() = handled %v err %v, want generic client error", handled, errRefresh)	}	diagnosticErr, ok := errRefresh.(interface{ LogDiagnostic() string })	if !ok || diagnosticErr.LogDiagnostic() != diagnostic {		t.Fatalf("log diagnostic = %T/%v, want %q", errRefresh, errRefresh, diagnostic)	}	if strings.Contains(errRefresh.Error(), diagnostic) || strings.Contains(errRefresh.Error(), "provider-secret") {		t.Fatalf("client error exposed internal detail: %v", errRefresh)	}} func TestRefreshAuthViaHomePreservesUpstreamStatusAndBodyExactly(t *testing.T) {	tests := []struct {		name   string		status int		body   []byte	}{		{name: "json", status: http.StatusBadRequest, body: []byte(`{"error":"invalid_request"}`)},		{name: "access token expired", status: http.StatusUnauthorized, body: []byte(`{"error":{"message":"access token expired"}}`)},		{name: "text", status: http.StatusBadGateway, body: []byte("provider unavailable")},		{name: "multiline", status: http.StatusTooManyRequests, body: []byte("first line\r\nsecond line\n")},		{name: "empty", status: http.StatusUnauthorized, body: []byte{}},	}	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			raw, errMarshal := json.Marshal(homeErrorEnvelope{Error: &homeErrorDetail{				Type:       "refresh_temporarily_unavailable",				Message:    "credential refresh temporarily unavailable",				Diagnostic: "antigravity refresh failed: stage=upstream_response status=400",				Upstream: &homeUpstreamResponse{					Status: tt.status,					Body:   tt.body,				},			}})			if errMarshal != nil {				t.Fatalf("marshal Home error envelope: %v", errMarshal)			}			client := &fakeHomeRefreshClient{raw: raw}			oldCurrentHomeRefreshClient := currentHomeRefreshClient			currentHomeRefreshClient = func() homeRefreshClient { return client }			t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 			cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}			auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "codex"}			_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)			statusErr, okStatus := errRefresh.(interface{ StatusCode() int })			if !handled || !okStatus || statusErr.StatusCode() != tt.status {				t.Fatalf("RefreshAuthViaHome() = handled %v err %v status %v, want true/%d", handled, errRefresh, okStatus, tt.status)			}			if got := []byte(errRefresh.Error()); !bytes.Equal(got, tt.body) {				t.Fatalf("refresh body = %q, want exact body %q", got, tt.body)			}			direct, okDirect := errRefresh.(interface {				DirectResponse() bool				ResponseBody() []byte			})			if !okDirect || !direct.DirectResponse() {				t.Fatalf("upstream error direct response = %v/%v, want true", okDirect, direct)			}			if got := direct.ResponseBody(); !bytes.Equal(got, tt.body) {				t.Fatalf("direct response body = %q, want exact body %q", got, tt.body)			}			diagnosticErr, okDiagnostic := errRefresh.(interface{ LogDiagnostic() string })			if !okDiagnostic || !strings.Contains(diagnosticErr.LogDiagnostic(), "stage=upstream_response") {				t.Fatalf("upstream log diagnostic = %T/%v", errRefresh, errRefresh)			}		})	}} func TestRefreshAuthViaHomeUsesGenericMessageForLegacyProviderDiagnostics(t *testing.T) {	tests := []struct {		name       string		provider   string		raw        []byte		wantStatus int		wantError  string		wantLog    string	}{		{			name:       "transient transport diagnostic",			provider:   "antigravity",			raw:        []byte(`{"error":{"type":"refresh_temporarily_unavailable","message":"antigravity refresh: Post https://oauth.example/token?access_token=provider-secret: connection refused"}}`),			wantStatus: http.StatusServiceUnavailable,			wantError:  "credential refresh temporarily unavailable",			wantLog:    "Home refresh failed: type=refresh_temporarily_unavailable",		},		{			name:       "terminal legacy diagnostic",			provider:   "codex",			raw:        []byte(`{"error":{"type":"authentication_error","message":"codex refresh: invalid_grant refresh_token=provider-secret"}}`),			wantStatus: http.StatusUnauthorized,			wantError:  "credential unauthorized",			wantLog:    "Home refresh failed: type=authentication_error",		},	}	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			client := &fakeHomeRefreshClient{raw: tt.raw}			oldCurrentHomeRefreshClient := currentHomeRefreshClient			currentHomeRefreshClient = func() homeRefreshClient { return client }			t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 			cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}			auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: tt.provider}			_, handled, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)			statusErr, okStatus := errRefresh.(interface{ StatusCode() int })			if !handled || !okStatus || statusErr.StatusCode() != tt.wantStatus {				t.Fatalf("RefreshAuthViaHome() = handled %v err %v, want status %d", handled, errRefresh, tt.wantStatus)			}			if got := errRefresh.Error(); got != tt.wantError {				t.Fatalf("client refresh error = %q, want %q", got, tt.wantError)			}			if strings.Contains(errRefresh.Error(), "provider-secret") {				t.Fatalf("legacy refresh error leaked provider detail: %v", errRefresh)			}			diagnosticErr, okDiagnostic := errRefresh.(interface{ LogDiagnostic() string })			if !okDiagnostic {				t.Fatalf("legacy refresh log diagnostic type = %T, want LogDiagnostic", errRefresh)			}			if got := diagnosticErr.LogDiagnostic(); got != tt.wantLog {				t.Fatalf("legacy refresh log diagnostic = %q, want %q", got, tt.wantLog)			}			if strings.Contains(diagnosticErr.LogDiagnostic(), "provider-secret") {				t.Fatalf("legacy refresh log diagnostic leaked provider detail: %q", diagnosticErr.LogDiagnostic())			}		})	}} func TestRefreshAuthViaHomeRejectsUnmarkedRefreshMessage(t *testing.T) {	client := &fakeHomeRefreshClient{raw: []byte(`{"error":{"type":"refresh_temporarily_unavailable","message":"database unavailable: provider-secret"}}`)}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient { return client }	t.Cleanup(func() { currentHomeRefreshClient = oldCurrentHomeRefreshClient }) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{ID: "home-auth", Index: "home-auth", Provider: "antigravity"}	_, _, errRefresh := RefreshAuthViaHome(context.Background(), cfg, auth)	if got, want := errRefresh.Error(), "credential refresh temporarily unavailable"; got != want {		t.Fatalf("refresh error = %q, want %q", got, want)	}} func TestAuthAccessTokenSHA256SupportsKnownMetadataShapes(t *testing.T) {	want := authAccessTokenSHA256(&cliproxyauth.Auth{Metadata: map[string]any{"access_token": "same-token"}})	cases := map[string]*cliproxyauth.Auth{		"camel case":        {Metadata: map[string]any{"accessToken": "same-token"}},		"nested any map":    {Metadata: map[string]any{"token": map[string]any{"access_token": "same-token"}}},		"nested string map": {Metadata: map[string]any{"Token": map[string]string{"accessToken": "same-token"}}},	}	for name, auth := range cases {		t.Run(name, func(t *testing.T) {			if got := authAccessTokenSHA256(auth); got == "" || got != want {				t.Fatalf("token hash = %q, want %q", got, want)			}		})	}} func TestRefreshAuthViaHomeAcceptsAuthEnvelope(t *testing.T) {	raw, errMarshal := json.Marshal(struct {		Auth      cliproxyauth.Auth `json:"auth"`		AuthIndex string            `json:"auth_index"`	}{		Auth: cliproxyauth.Auth{			ID:       "home-auth-1",			Provider: "antigravity",			Metadata: map[string]any{				"access_token": "new-access-token",			},		},		AuthIndex: "home-index-1",	})	if errMarshal != nil {		t.Fatalf("marshal home envelope: %v", errMarshal)	} 	client := &fakeHomeRefreshClient{raw: raw}	oldCurrentHomeRefreshClient := currentHomeRefreshClient	currentHomeRefreshClient = func() homeRefreshClient {		return client	}	t.Cleanup(func() {		currentHomeRefreshClient = oldCurrentHomeRefreshClient	}) 	cfg := &config.Config{Home: config.HomeConfig{Enabled: true}}	auth := &cliproxyauth.Auth{		ID:       "home-auth-1",		Provider: "antigravity",		Index:    "home-index-1",		Metadata: map[string]any{			"access_token":  "old-access-token",			"refresh_token": "refresh-token",		},	} 	updated, handled, err := RefreshAuthViaHome(context.Background(), cfg, auth)	if err != nil {		t.Fatalf("RefreshAuthViaHome error: %v", err)	}	if !handled {		t.Fatal("RefreshAuthViaHome handled = false, want true")	}	if got := client.calls.Load(); got != 1 {		t.Fatalf("home refresh calls = %d, want 1", got)	}	if client.authIndex != "home-index-1" {		t.Fatalf("home refresh auth_index = %q, want home-index-1", client.authIndex)	}	if client.accessTokenHash != authAccessTokenSHA256(auth) {		t.Fatalf("home refresh access token hash = %q, want %q", client.accessTokenHash, authAccessTokenSHA256(auth))	}	if updated == nil {		t.Fatal("updated auth = nil")	}	if got := updated.Metadata["access_token"]; got != "new-access-token" {		t.Fatalf("updated access_token = %q, want new-access-token", got)	}	if updated.Index != "home-index-1" {		t.Fatalf("updated auth_index = %q, want home-index-1", updated.Index)	}}