package pier_test import ( "bytes" "context" "io" "net/http" "strings" "testing" "time" "git.golem15.com/golem15/summercms/modules/pact" ) // TestEndpointGuards (T-19.7-18, T-19.7-19, Pitfall 10): the endpoint's own // outer layers. A browser Origin other than the allowed ones is 403 before // the bearer or the body is read (no Origin and the app's own origin pass); // a request body above the endpoint's cap is 413; and a long-lived GET // stream re-verifies its bearer and ends once the grant no longer verifies. func TestEndpointGuards(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() verifier := newFakeVerifier(map[string]pact.MCPGrant{"oas_at_stream": grant(12, "c6", 6, "read")}) in := startInstance(t, newApp(t, appSpec{guards: []pact.MCPGuard{&acmeGuard{}}, verifier: verifier}), acmePlugin{tools: []pact.MCPTool{echoTool()}}) post := func(token, origin string, body io.Reader) *http.Response { t.Helper() req, err := http.NewRequestWithContext(ctx, http.MethodPost, in.url(), body) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") req.Header.Set("Mcp-Protocol-Version", "2025-11-25") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } if origin != "" { req.Header.Set("Origin", origin) } res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } _, _ = io.Copy(io.Discard, res.Body) _ = res.Body.Close() return res } t.Run("origin check", func(t *testing.T) { if res := post("", "https://evil.example", strings.NewReader(initializeBody)); res.StatusCode != http.StatusForbidden { t.Fatalf("foreign origin without a bearer = %d, want 403 before the bearer check", res.StatusCode) } if res := post("oas_at_test", "https://evil.example", strings.NewReader(initializeBody)); res.StatusCode != http.StatusForbidden { t.Fatalf("foreign origin = %d", res.StatusCode) } if res := post("oas_at_test", "http://acme.test", strings.NewReader(initializeBody)); res.StatusCode != http.StatusOK { t.Fatalf("the app's own origin = %d", res.StatusCode) } if res := post("oas_at_test", "", strings.NewReader(initializeBody)); res.StatusCode != http.StatusOK { t.Fatalf("no origin = %d", res.StatusCode) } }) t.Run("body cap", func(t *testing.T) { big := bytes.Repeat([]byte(" "), 5<<20) body := append([]byte(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`), big...) if res := post("oas_at_test", "", bytes.NewReader(body)); res.StatusCode != http.StatusRequestEntityTooLarge { t.Fatalf("5 MiB body = %d, want 413", res.StatusCode) } }) t.Run("stream re-verifies its bearer", func(t *testing.T) { in.pier().SetRecheck(50 * time.Millisecond) sid := openSession(t, ctx, in, "oas_at_stream") req, err := http.NewRequestWithContext(ctx, http.MethodGet, in.url(), nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer oas_at_stream") req.Header.Set("Accept", "text/event-stream") req.Header.Set("Mcp-Session-Id", sid) req.Header.Set("Mcp-Protocol-Version", "2025-11-25") res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { t.Fatalf("GET stream = %d", res.StatusCode) } ended := make(chan struct{}) go func() { _, _ = io.Copy(io.Discard, res.Body) close(ended) }() select { case <-ended: t.Fatal("the stream ended while the grant still verified") case <-time.After(300 * time.Millisecond): } verifier.revoke("oas_at_stream") select { case <-ended: case <-time.After(5 * time.Second): t.Fatal("the stream outlived its revoked grant") } }) }