turnstile-caddy/turnstile_test.go
Lukas Schaefer d38dff43a1
All checks were successful
/ resolve-go (push) Successful in 3s
/ test (push) Successful in 6s
/ cleanup (push) Successful in 1s
Implement remember me support to survive browser restarts
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-09-12 23:24:56 -04:00

149 lines
4.6 KiB
Go

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)
}
}