test: Prozessgrenzen-Tests für CORS-Tür und MCP-Handshake

CORS: Preflight 204+Header von gelisteter Origin, kein ACAO für ungeliste/
leere Liste, Wildcard, nur-/speak, cors-Feld in /status. MCP: kompletter
Handshake als literal JSON-RPC über HTTP, tools/call enqueue sichtbar am
Fake-Provider, muted/rate_limit/quiet_hours als isError:false-Ergebnisse,
Fehlerpfade (-32700/-32600/-32601/-32602), GET /mcp 405.
This commit is contained in:
m3ta-chiron committed 2026-10-18 20:06:11 +02:00
1 parent a577597714
commit 150c374bdc
3 files changed
+565 -2

No files matched your search

+70 -2
View File
@@ -13,6 +13,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
@@ -401,8 +402,10 @@ type statusBody struct {
Text string `json:"text"`
Priority string `json:"priority"`
} `json:"current"`
Muted bool `json:"muted"`
Provider string `json:"provider"`
Muted bool `json:"muted"`
Provider string `json:"provider"`
MCP bool `json:"mcp"`
Cors []string `json:"cors"`
}
// getStatus ruft GET /status auf (Deadline: eine Sekunde — der Daemon ist
@@ -471,3 +474,68 @@ func postPath(t *testing.T, d *daemonProc, path string) (int, map[string]any) {
}
return resp.StatusCode, m
}
// rawResponse ist eine vollständig gelesene HTTP-Antwort (Status, Header,
// Body als String) — Beobachtungsform der rohen CORS-/MCP-Tests.
type rawResponse struct {
status int
header http.Header
body string
}
// doRequest macht einen rohen HTTP-Request mit beliebigen Headern und
// optionalem Body und liest die Antwort komplett (CORS-Preflights und
// MCP-JSON-RPC-Payloads sind literal, kein Client-SDK dazwischen).
func doRequest(t *testing.T, method, url string, headers map[string]string, body string) rawResponse {
t.Helper()
var rd io.Reader
if body != "" {
rd = strings.NewReader(body)
}
req, err := http.NewRequest(method, url, rd)
if err != nil {
t.Fatalf("%s %s bauen: %v", method, url, err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("%s %s: %v", method, url, err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("%s %s Antwort lesen: %v", method, url, err)
}
return rawResponse{status: resp.StatusCode, header: resp.Header, body: string(raw)}
}
// preflight macht einen OPTIONS-Preflight gegen /speak (so wie ein Browser
// es vor einem echten POST tut).
func preflight(t *testing.T, d *daemonProc, origin string) rawResponse {
t.Helper()
return doRequest(t, http.MethodOptions, d.baseURL+"/speak", map[string]string{
"Origin": origin,
"Access-Control-Request-Method": http.MethodPost,
}, "")
}
// mcpPost POSTet einen literalen JSON-RPC-Payload an /mcp.
func mcpPost(t *testing.T, d *daemonProc, payload string) rawResponse {
t.Helper()
return doRequest(t, http.MethodPost, d.baseURL+"/mcp", map[string]string{
"Content-Type": "application/json",
}, payload)
}
// mcpDecode dekodiert eine JSON-RPC-Antwort in eine generische Map (für
// Feld-Zugriffe der literalen Payload-Tests).
func mcpDecode(t *testing.T, raw rawResponse) map[string]any {
t.Helper()
var m map[string]any
if err := json.Unmarshal([]byte(raw.body), &m); err != nil {
t.Fatalf("JSON-RPC-Antwort dekodieren: %v — Body: %s", err, raw.body)
}
return m
}