Files
CLIProxyAPI/internal/api/server_devin_oauth_test.go
Luis Pater cca35aee93 fix(devin): use loopback callback endpoint for oauth redirect uri
- Enforce `http://127.0.0.1:<port>/callback` redirect URI to satisfy Devin authorization validation requirements.
- Register `/callback` route while preserving `/devin/callback` handler.
2026-09-14 09:37:35 +08:00

82 lines
2.8 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
management "github.com/router-for-me/CLIProxyAPI/v7/internal/api/handlers/management"
)
func TestDevinOAuthRoutes(t *testing.T) {
t.Setenv("MANAGEMENT_PASSWORD", "test-management-key")
server := newTestServer(t)
w := httptest.NewRecorder()
server.engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/v0/management/devin-auth-url", nil))
if w.Code != http.StatusUnauthorized {
t.Fatalf("unprotected login route: %d", w.Code)
}
registered := false
for _, route := range server.engine.Routes() {
if route.Method == http.MethodGet && route.Path == "/v0/management/devin-auth-url" {
registered = true
}
}
if !registered {
t.Fatal("Devin login route not registered")
}
for _, test := range []struct {
name, provider, query, callbackPath string
want int
}{
{name: "success", provider: "devin", query: "code=test-code", callbackPath: "/callback", want: http.StatusOK},
{name: "success legacy path", provider: "devin", query: "code=test-code", callbackPath: "/devin/callback", want: http.StatusOK},
{name: "denied", provider: "devin", query: "error=access_denied", callbackPath: "/callback", want: http.StatusOK},
{name: "wrong provider", provider: "codex", query: "code=test-code", callbackPath: "/callback", want: http.StatusBadRequest},
{name: "missing code", provider: "devin", callbackPath: "/callback", want: http.StatusBadRequest},
{name: "unknown state", query: "code=test-code", callbackPath: "/callback", want: http.StatusBadRequest},
} {
t.Run(test.name, func(t *testing.T) {
state := "devin-route-" + strings.ReplaceAll(test.name, " ", "-")
if test.provider != "" {
management.RegisterOAuthSession(state, test.provider)
defer management.CompleteOAuthSession(state)
}
w := httptest.NewRecorder()
path := test.callbackPath
if path == "" {
path = "/callback"
}
server.engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path+"?state="+state+"&"+test.query, nil))
if w.Code != test.want {
t.Fatalf("callback: %d %s", w.Code, w.Body.String())
}
filePath := filepath.Join(server.cfg.AuthDir, ".oauth-devin-"+state+".oauth")
data, errRead := os.ReadFile(filePath)
if test.want == http.StatusOK {
if errRead != nil {
t.Fatal(errRead)
}
var payload map[string]string
if errDecode := json.Unmarshal(data, &payload); errDecode != nil {
t.Fatal(errDecode)
}
if payload["state"] != state || (payload["code"] == "" && payload["error"] == "") {
t.Fatalf("callback payload: %v", payload)
}
if w.Header().Get("Cache-Control") != "no-store" {
t.Fatal("callback response is cacheable")
}
} else if !os.IsNotExist(errRead) {
t.Fatalf("invalid callback persisted: %v", errRead)
}
})
}
}