package turnstile import ( "bytes" "context" "html/template" "io" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) type captureTransport struct { remoteIP string } func (t *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) { body, err := io.ReadAll(req.Body) if err != nil { return nil, err } vals, err := url.ParseQuery(string(body)) if err != nil { return nil, err } t.remoteIP = vals.Get("remoteip") return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte(`{"success":true}`))), Header: make(http.Header), }, nil } func TestVerifyTurnstileSendsTrustedClientIP(t *testing.T) { transport := &captureTransport{} m := Middleware{ SecretKey: "secret", httpClient: &http.Client{Transport: transport}, } req := httptestNewRequestWithTrustedClientIP("192.0.2.1") req.RemoteAddr = "203.0.113.1:443" ok, err := m.verifyTurnstile(req, "turnstile-token") if err != nil { t.Fatal(err) } if !ok { t.Fatal("expected successful verification") } if transport.remoteIP != "192.0.2.1" { t.Fatalf("remoteip = %q, want 192.0.2.1", transport.remoteIP) } } func httptestNewRequestWithTrustedClientIP(ip string) *http.Request { req := httptest.NewRequest(http.MethodPost, "https://example.com/__turnstile__/verify", nil) vars := map[string]any{caddyhttp.ClientIPVarKey: ip} ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) return req.WithContext(ctx) } func TestChallengeReason(t *testing.T) { signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour) if err != nil { t.Fatal(err) } enabled := Middleware{ ForgejoCookieName: "i_like_gitea", sessionPasses: signer, } disabled := Middleware{} anon := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil) if got := enabled.challengeReason(anon); got != challengeAnonymous { t.Fatalf("no credentials: got %q, want %q", got, challengeAnonymous) } if got := disabled.challengeReason(anon); got != challengeAnonymous { t.Fatalf("forgejo disabled: got %q, want %q", got, challengeAnonymous) } withCookie := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil) withCookie.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "stale-session"}) if got := enabled.challengeReason(withCookie); got != challengeInvalid { t.Fatalf("invalid session cookie: got %q, want %q", got, challengeInvalid) } withRemember := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil) enabled.ForgejoRememberCookieName = "persistent" withRemember.AddCookie(&http.Cookie{Name: "persistent", Value: "remember-token"}) if got := enabled.challengeReason(withRemember); got != challengeInvalid { t.Fatalf("invalid remember-me cookie: got %q, want %q", got, challengeInvalid) } withBearer := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil) withBearer.Header.Set("Authorization", "Bearer bad-token") if got := enabled.challengeReason(withBearer); got != challengeInvalid { t.Fatalf("invalid bearer: got %q, want %q", got, challengeInvalid) } if got := disabled.challengeReason(withBearer); got != challengeAnonymous { t.Fatalf("bearer ignored when forgejo disabled: got %q, want %q", got, challengeAnonymous) } } func TestServeChallengeSetsHeaderAndOK(t *testing.T) { tmpl, err := template.New("challenge").Parse(challengeHTML) if err != nil { t.Fatal(err) } signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour) if err != nil { t.Fatal(err) } m := Middleware{ SiteKey: "site-key", verify: "/__turnstile__/verify", tmpl: tmpl, ForgejoCookieName: "i_like_gitea", sessionPasses: signer, } rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo", nil) if err := m.serveChallenge(rec, req, ""); err != nil { t.Fatal(err) } if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get(challengeHeader); got != challengeAnonymous { t.Fatalf("header = %q, want %q", got, challengeAnonymous) } rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo", nil) req.Header.Set("Authorization", "token bad") if err := m.serveChallenge(rec, req, ""); err != nil { t.Fatal(err) } if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get(challengeHeader); got != challengeInvalid { t.Fatalf("header = %q, want %q", got, challengeInvalid) } }