Ctrl K
ComunidadGuías…

internal/api/server_siwc_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package api

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/api/handlers/management"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/auth/codex"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
	coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
	"golang.org/x/crypto/bcrypt"
)

func TestSIWCManagementRouteAccessControl(t *testing.T) {
	hash, errHash := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.MinCost)
	if errHash != nil {
		t.Fatal(errHash)
	}
	for _, tc := range []struct {
		name                      string
		enabled, home, authorized bool
		want                      int
	}{
		{"authorized invalid target", true, false, true, http.StatusBadRequest},
		{"missing management key", true, false, false, http.StatusUnauthorized},
		{"management disabled", false, false, true, http.StatusNotFound},
		{"home disabled local API", true, true, true, http.StatusNotFound},
	} {
		t.Run(tc.name, func(t *testing.T) {
			cfg := &config.Config{AuthDir: t.TempDir()}
			cfg.Home.Enabled = tc.home
			cfg.RemoteManagement.SecretKey = string(hash)
			h := management.NewHandlerWithoutConfigFilePath(cfg, nil)
			h.SetLocalPassword("test-password")
			s := &Server{cfg: cfg, engine: gin.New(), mgmt: h}
			s.managementRoutesEnabled.Store(tc.enabled)
			s.registerManagementRoutes()
			req := httptest.NewRequest(http.MethodPost, "/v8/management/oauth/siwc/bind", strings.NewReader(`{}`))
			req.Header.Set("Content-Type", "application/json")
			req.RemoteAddr = "127.0.0.1:1234"
			if tc.authorized {
				req.Header.Set("Authorization", "Bearer test-password")
			}
			w := httptest.NewRecorder()
			s.engine.ServeHTTP(w, req)
			if w.Code != tc.want {
				t.Fatalf("status=%d want=%d: %s", w.Code, tc.want, w.Body.String())
			}
			for _, route := range s.engine.Routes() {
				if strings.HasPrefix(route.Path, "/v0/") && strings.Contains(route.Path, "siwc") {
					t.Fatal("SIWC route registered under v0")
				}
			}
		})
	}
}

func TestCodexCallbackRejectsSIWCState(t *testing.T) {
	hash, errHash := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.MinCost)
	if errHash != nil {
		t.Fatal(errHash)
	}
	cfg := &config.Config{}
	cfg.RemoteManagement.SecretKey = string(hash)
	s := newTestServerWithConfig(t, cfg)
	metadata := map[string]any{"type": "codex", "access_token": "outer-token", "refresh_token": "outer-refresh"}
	data, err := json.Marshal(metadata)
	if err != nil {
		t.Fatal(err)
	}
	path := filepath.Join(s.cfg.AuthDir, "codex.json")
	if errWrite := os.WriteFile(path, data, 0o600); errWrite != nil {
		t.Fatal(errWrite)
	}
	manager := coreauth.NewManager(nil, nil, nil)
	manager.SetConfig(s.cfg)
	if _, errRegister := manager.Register(context.Background(), &coreauth.Auth{
		ID: "codex.json", FileName: "codex.json", Provider: "codex", Status: coreauth.StatusActive,
		Metadata: metadata, Attributes: map[string]string{"path": path},
	}); errRegister != nil {
		t.Fatal(errRegister)
	}
	s.mgmt.SetAuthManager(manager)
	s.mgmt.SetLocalPassword("test-password")
	s.managementRoutesEnabled.Store(true)
	s.registerManagementRoutes()
	req := httptest.NewRequest(http.MethodPost, "/v8/management/oauth/siwc/bind", strings.NewReader(`{"name":"codex.json"}`))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-password")
	req.RemoteAddr = "127.0.0.1:1234"
	started := httptest.NewRecorder()
	s.engine.ServeHTTP(started, req)
	if started.Code != http.StatusOK {
		t.Fatalf("start status=%d: %s", started.Code, started.Body.String())
	}
	var flow struct {
		State string `json:"state"`
		Flow  string `json:"flow"`
	}
	if errDecode := json.Unmarshal(started.Body.Bytes(), &flow); errDecode != nil {
		t.Fatal(errDecode)
	}
	if flow.State == "" || flow.Flow != "siwc_bind" {
		t.Fatalf("unexpected bind response: %s", started.Body.String())
	}
	t.Cleanup(func() { management.CancelOAuthSession(flow.State) })
	callback := httptest.NewRecorder()
	query := url.Values{"state": {flow.State}, "code": {"issued-code"}, "client_id": {"oaiapp_issued"}}
	s.engine.ServeHTTP(callback, httptest.NewRequest(http.MethodGet, "/codex/callback?"+query.Encode(), nil))
	if callback.Code != http.StatusConflict {
		t.Fatalf("callback status=%d want=409: %s", callback.Code, callback.Body.String())
	}
	if !strings.Contains(callback.Body.String(), "/v8/management/oauth/callback") {
		t.Fatalf("missing v8 callback guidance: %s", callback.Body.String())
	}
	entries, errRead := os.ReadDir(s.cfg.AuthDir)
	if errRead != nil {
		t.Fatal(errRead)
	}
	for _, entry := range entries {
		if strings.HasPrefix(entry.Name(), ".oauth") {
			t.Fatalf("legacy callback created OAuth file %q", entry.Name())
		}
	}
	if !management.IsOAuthSessionPending(flow.State, "codex") {
		t.Fatal("rejected callback consumed SIWC session")
	}
}

func TestSIWCStatusEndpoint(t *testing.T) {
	hash, errHash := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.MinCost)
	if errHash != nil {
		t.Fatal(errHash)
	}
	cfg := &config.Config{}
	cfg.RemoteManagement.SecretKey = string(hash)
	s := newTestServerWithConfig(t, cfg)
	metadata := map[string]any{"type": "codex", "access_token": "outer-token", "refresh_token": "outer-refresh"}
	path := filepath.Join(s.cfg.AuthDir, "codex.json")
	data, err := json.Marshal(metadata)
	if err != nil {
		t.Fatal(err)
	}
	if errWrite := os.WriteFile(path, data, 0o600); err != nil || errWrite != nil {
		t.Fatal(errWrite)
	}
	manager := coreauth.NewManager(nil, nil, nil)
	manager.SetConfig(s.cfg)
	if _, errRegister := manager.Register(context.Background(), &coreauth.Auth{
		ID: "codex.json", FileName: "codex.json", Provider: "codex", Status: coreauth.StatusActive,
		Metadata: metadata, Attributes: map[string]string{"path": path},
	}); errRegister != nil {
		t.Fatal(errRegister)
	}
	s.mgmt.SetAuthManager(manager)
	s.mgmt.SetLocalPassword("test-password")
	s.managementRoutesEnabled.Store(true)
	s.registerManagementRoutes()

	checkStatus := func(url string, wantCode int, wantBody string) {
		t.Helper()
		req := httptest.NewRequest(http.MethodGet, url, nil)
		req.Header.Set("Authorization", "Bearer test-password")
		req.RemoteAddr = "127.0.0.1:1234"
		w := httptest.NewRecorder()
		s.engine.ServeHTTP(w, req)
		if w.Code != wantCode {
			t.Fatalf("%s: status=%d want=%d: %s", url, w.Code, wantCode, w.Body.String())
		}
		if wantBody != "" && strings.TrimSpace(w.Body.String()) != wantBody {
			t.Fatalf("%s: body=%s want %s", url, w.Body.String(), wantBody)
		}
	}

	// 1. use_siwc not enabled -> off
	checkStatus("/v8/management/oauth/siwc/status?name=codex.json", http.StatusOK, `{"status":"off"}`)
	checkStatus("/v8/management/credentials/siwc/status?name=codex.json", http.StatusOK, `{"status":"off"}`)

	// 2. Bind SIWC with valid future expires_at -> valid
	registered, _ := manager.GetByID("codex.json")
	_, errBind := manager.BindCodexSIWC(context.Background(), "codex.json", registered.RegistrationEpoch, "", &codex.CodexSIWCTokenData{
		AccessToken:  "siwc-valid-1",
		RefreshToken: "siwc-refresh-1",
		ClientID:     "oaiapp_1",
		Expire:       "2035-01-01T00:00:00Z",
		ExpiresAt:    "2035-01-01T00:03:00Z",
	})
	if errBind != nil {
		t.Fatalf("BindCodexSIWC: %v", errBind)
	}
	bound, _ := manager.GetByID("codex.json")
	bound.EnsureIndex()
	checkStatus("/v8/management/oauth/siwc/status?name=codex.json", http.StatusOK, `{"status":"valid"}`)
	checkStatus("/v8/management/oauth/siwc/status?auth_index="+bound.Index, http.StatusOK, `{"status":"valid"}`)

	// 3. SIWC request receives 401 -> invalid
	manager.MarkResult(context.Background(), coreauth.Result{
		AuthID:            "codex.json",
		CredentialVersion: bound.CredentialVersion,
		Provider:          "codex",
		Model:             "gpt-5.4",
		Success:           false,
		Error:             &coreauth.Error{HTTPStatus: http.StatusUnauthorized, Code: "unauthorized", Message: "401 unauthorized"},
	})
	checkStatus("/v8/management/oauth/siwc/status?name=codex.json", http.StatusOK, `{"status":"invalid"}`)

	// 4. Missing parameter or unknown target
	checkStatus("/v8/management/oauth/siwc/status", http.StatusBadRequest, "")
	checkStatus("/v8/management/oauth/siwc/status?name=missing.json", http.StatusNotFound, "")
}