extractToken accepted Authorization values with no scheme prefix, silently treating them as per-request tokens. The stateless-http-auth spec mandates bare tokens be rejected and treated as no header. Drop the fallback branch so unrecognized/scheme-less values fall through to the global singleton instead of forging an identity. - operation: extractToken returns "" for scheme-less headers (task 2.5) - test: extractToken case-insensitivity + bare-token rejection (4.4) - test: ephemeral-client construction failure surfaces error, no singleton downgrade (4.3) - docs: cross-link stateless-http-auth OpenSpec change from README (5.2) - tasks: mark completed blocker fixes (1.3, 2.4, 2.5, 4.2-4.4, 5.x, 6.3)
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package forgejo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/flag"
|
|
)
|
|
|
|
func TestClient_WithContextToken(t *testing.T) {
|
|
// Need a real httptest server because forgejo.NewClient makes a test request
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"version":"1.22.0"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Setup flag defaults
|
|
flag.URL = srv.URL
|
|
flag.Token = "global-token"
|
|
// Reset the singleton
|
|
client = nil
|
|
clientOnce = sync.Once{}
|
|
|
|
// 1. Get client without context token
|
|
ctx := context.Background()
|
|
c1, err := Client(ctx)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if c1 == nil {
|
|
t.Fatalf("expected non-nil client")
|
|
}
|
|
|
|
// 2. Get client with context token
|
|
ctx2 := WithToken(context.Background(), "request-token")
|
|
c2, err := Client(ctx2)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if c2 == nil {
|
|
t.Fatalf("expected non-nil client")
|
|
}
|
|
|
|
if c1 == c2 {
|
|
t.Fatalf("expected a new ephemeral client instance when token is present, got the same singleton")
|
|
}
|
|
}
|
|
|
|
// TestClient_EphemeralConstructionError asserts that when a context token is
|
|
// present but forgejo.NewClient fails (here: unreachable URL so the SDK's
|
|
// version probe errors), Client returns an error and a nil client rather than
|
|
// silently downgrading to the global singleton. Spec: "Failed ephemeral
|
|
// construction MUST NOT fall through to the singleton."
|
|
func TestClient_EphemeralConstructionError(t *testing.T) {
|
|
// Port 1 refuses connections, so the SDK's server-version probe fails.
|
|
flag.URL = "http://127.0.0.1:1"
|
|
flag.Token = "global-token"
|
|
client = nil
|
|
clientOnce = sync.Once{}
|
|
|
|
ctx := WithToken(context.Background(), "request-token")
|
|
c, err := Client(ctx)
|
|
if err == nil {
|
|
t.Fatalf("expected error when ephemeral client construction fails, got nil")
|
|
}
|
|
if c != nil {
|
|
t.Fatalf("expected nil client on construction failure, got non-nil (silent singleton downgrade)")
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthorizationHeader(t *testing.T) {
|
|
tokensSeen := make(map[string]bool)
|
|
var mu sync.Mutex
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth != "" {
|
|
mu.Lock()
|
|
tokensSeen[auth] = true
|
|
mu.Unlock()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
// Mock responses for both /version and /user
|
|
if r.URL.Path == "/api/v1/version" {
|
|
_, _ = w.Write([]byte(`{"version":"1.22.0"}`))
|
|
} else {
|
|
_, _ = w.Write([]byte(`{"login":"test"}`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
flag.URL = srv.URL
|
|
flag.Token = "global-token"
|
|
client = nil
|
|
clientOnce = sync.Once{}
|
|
|
|
// Test concurrent requests with different tokens
|
|
var wg sync.WaitGroup
|
|
numRequests := 10
|
|
for i := 0; i < numRequests; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
token := fmt.Sprintf("token-%d", id)
|
|
ctx := WithToken(context.Background(), token)
|
|
c, err := Client(ctx)
|
|
if err != nil {
|
|
t.Errorf("failed to get client: %v", err)
|
|
return
|
|
}
|
|
_, _, _ = c.GetMyUserInfo()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for i := 0; i < numRequests; i++ {
|
|
expected := fmt.Sprintf("token token-%d", i)
|
|
if !tokensSeen[expected] {
|
|
t.Errorf("expected to see token %q, but didn't", expected)
|
|
}
|
|
}
|
|
}
|