From fc0615b171213b5aa8482e2110b30900feb2c842 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 29 May 2026 23:04:35 +0800 Subject: [PATCH] test(oauth): ensure missing auth directories are created and callback payloads are validated Closes: #3619 --- .../api/handlers/management/oauth_callback.go | 2 + .../management/oauth_callback_test.go | 82 +++++++++++++++++++ .../api/handlers/management/oauth_sessions.go | 3 + 3 files changed, 87 insertions(+) create mode 100644 internal/api/handlers/management/oauth_callback_test.go diff --git a/internal/api/handlers/management/oauth_callback.go b/internal/api/handlers/management/oauth_callback.go index c7f7be5e..251f999e 100644 --- a/internal/api/handlers/management/oauth_callback.go +++ b/internal/api/handlers/management/oauth_callback.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) type oauthCallbackRequest struct { @@ -97,6 +98,7 @@ func (h *Handler) PostOAuthCallback(c *gin.Context) { c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "oauth flow is not pending"}) return } + log.WithError(errWrite).Error("failed to persist oauth callback") c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to persist oauth callback"}) return } diff --git a/internal/api/handlers/management/oauth_callback_test.go b/internal/api/handlers/management/oauth_callback_test.go new file mode 100644 index 00000000..a9ff971f --- /dev/null +++ b/internal/api/handlers/management/oauth_callback_test.go @@ -0,0 +1,82 @@ +package management + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" +) + +func TestPostOAuthCallbackCreatesMissingAuthDir(t *testing.T) { + gin.SetMode(gin.TestMode) + + authDir := filepath.Join(t.TempDir(), "missing-auth") + state := "test-antigravity-state" + RegisterOAuthSession(state, "antigravity") + defer CompleteOAuthSession(state) + + h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: authDir}, nil) + router := gin.New() + router.POST("/v0/management/oauth-callback", h.PostOAuthCallback) + + body := `{"provider":"antigravity","redirect_url":"http://localhost:59788/oauth-callback?state=test-antigravity-state&code=test-code"}` + req := httptest.NewRequest(http.MethodPost, "/v0/management/oauth-callback", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d with body %s", http.StatusOK, w.Code, w.Body.String()) + } + + callbackPath := filepath.Join(authDir, ".oauth-antigravity-"+state+".oauth") + data, errRead := os.ReadFile(callbackPath) + if errRead != nil { + t.Fatalf("expected callback file to be written: %v", errRead) + } + + var payload oauthCallbackFilePayload + if errUnmarshal := json.Unmarshal(data, &payload); errUnmarshal != nil { + t.Fatalf("failed to decode callback payload: %v", errUnmarshal) + } + if payload.State != state || payload.Code != "test-code" || payload.Error != "" { + t.Fatalf("unexpected callback payload: %+v", payload) + } +} + +func TestWriteOAuthCallbackFileForPendingSessionCreatesMissingAuthDirForCallbackProviders(t *testing.T) { + providers := []string{"anthropic", "codex", "gemini", "antigravity", "xai"} + for _, provider := range providers { + t.Run(provider, func(t *testing.T) { + authDir := filepath.Join(t.TempDir(), "missing-auth") + state := provider + "-state" + RegisterOAuthSession(state, provider) + defer CompleteOAuthSession(state) + + path, errWrite := WriteOAuthCallbackFileForPendingSession(authDir, provider, state, "code-"+provider, "") + if errWrite != nil { + t.Fatalf("expected callback file write to succeed: %v", errWrite) + } + + data, errRead := os.ReadFile(path) + if errRead != nil { + t.Fatalf("expected callback file to be written: %v", errRead) + } + + var payload oauthCallbackFilePayload + if errUnmarshal := json.Unmarshal(data, &payload); errUnmarshal != nil { + t.Fatalf("failed to decode callback payload: %v", errUnmarshal) + } + if payload.State != state || payload.Code != "code-"+provider || payload.Error != "" { + t.Fatalf("unexpected callback payload: %+v", payload) + } + }) + } +} diff --git a/internal/api/handlers/management/oauth_sessions.go b/internal/api/handlers/management/oauth_sessions.go index a74f7d56..d861b788 100644 --- a/internal/api/handlers/management/oauth_sessions.go +++ b/internal/api/handlers/management/oauth_sessions.go @@ -269,6 +269,9 @@ func WriteOAuthCallbackFile(authDir, provider, state, code, errorMessage string) fileName := fmt.Sprintf(".oauth-%s-%s.oauth", canonicalProvider, state) filePath := filepath.Join(authDir, fileName) + if err := os.MkdirAll(authDir, 0o700); err != nil { + return "", fmt.Errorf("create oauth callback dir: %w", err) + } payload := oauthCallbackFilePayload{ Code: strings.TrimSpace(code), State: strings.TrimSpace(state), -- 2.51.2