- Reject non-loopback bind/upstream, cap bodies, isolate concurrent sessions and refuse traversal - Scrub JWT, inv_ tokens, cookies, client secrets and OAuth codes out of fixtures - CLI discovery and replay errors exit nonzero without printing secrets Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.4 KiB
Go
158 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/bonfire"
|
|
"git.golem15.com/golem15/summercms/tide"
|
|
)
|
|
|
|
func TestParityCommandContract(t *testing.T) {
|
|
names := commandNames()
|
|
for _, want := range []string{"parity:record", "parity:proxy", "parity:replay"} {
|
|
if !containsName(names, want) {
|
|
t.Fatalf("missing %s in %v", want, names)
|
|
}
|
|
}
|
|
|
|
spec := filepath.Join("..", "..", "tide", "testdata", "one-route-spec.yaml")
|
|
outDir := t.TempDir()
|
|
fixture := filepath.Join(outDir, "sample.yaml")
|
|
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSJ9.signaturehere123456"
|
|
const inv = "inv_abcd1234xyz"
|
|
|
|
orig := httptest.NewServer(jsonHandler(`{"data":"ok","token":"` + jwt + `"}`))
|
|
t.Cleanup(orig.Close)
|
|
changed := httptest.NewServer(jsonHandler(`{"data":"no","token":"` + jwt + `"}`))
|
|
t.Cleanup(changed.Close)
|
|
|
|
varsPath := filepath.Join(t.TempDir(), "vars.yaml")
|
|
rulesPath := filepath.Join(outDir, "rules.yaml")
|
|
if err := os.WriteFile(rulesPath, []byte("client: nuxt\nkeep_request_headers: []\nkeep_response_headers:\n - Content-Type\nroutes:\n - method: GET\n path: /sample\n capture:\n - from: response.json\n path: $.token\n as: jwt:alice\n category: jwt\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
out, err := runParityCapture("parity:record", "--spec", spec, "--target", orig.URL, "--output", fixture, "--rules", rulesPath, "--vars", varsPath)
|
|
if err != nil {
|
|
t.Fatalf("record: %v\n%s", err, out)
|
|
}
|
|
raw, err := os.ReadFile(fixture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(raw)
|
|
if strings.Contains(text, jwt) || strings.Contains(text, inv) {
|
|
t.Fatalf("recorded YAML leaked secret:\n%s", text)
|
|
}
|
|
if !strings.Contains(text, "{{jwt:alice}}") {
|
|
t.Fatalf("recorded YAML missing placeholder:\n%s", text)
|
|
}
|
|
assertNoSecrets(t, out+"\n"+text, jwt, inv)
|
|
|
|
out, err = runParityCapture("parity:replay", "--fixtures", fixture, "--target", orig.URL, "--vars", varsPath)
|
|
if err != nil {
|
|
t.Fatalf("replay identical: %v\n%s", err, out)
|
|
}
|
|
|
|
out, err = runParityCapture("parity:replay", "--fixtures", fixture, "--target", changed.URL, "--vars", varsPath)
|
|
if err == nil {
|
|
t.Fatal("changed JSON must fail with nonzero exit")
|
|
}
|
|
combined := out + "\n" + err.Error()
|
|
if !strings.Contains(err.Error(), "$.data") {
|
|
t.Fatalf("mismatch missing $.data: %s", combined)
|
|
}
|
|
assertNoSecrets(t, combined, jwt, inv)
|
|
|
|
out, err = runParityCapture("parity:record", "--spec", spec, "--target", orig.URL)
|
|
if err == nil || !strings.Contains(err.Error(), "--output") {
|
|
t.Fatalf("missing output must fail: %v %s", err, out)
|
|
}
|
|
assertNoSecrets(t, out+"\n"+errString(err), jwt, inv)
|
|
|
|
out, err = runParityCapture("parity:proxy", "--rules", rulesPath, "--fixtures", outDir, "--upstream", "http://example.com")
|
|
if err == nil || !strings.Contains(err.Error(), "loopback") {
|
|
t.Fatalf("proxy non-loopback: %v %s", err, out)
|
|
}
|
|
assertNoSecrets(t, out+"\n"+errString(err), jwt, inv)
|
|
|
|
missing := filepath.Join(outDir, "missing-var.yaml")
|
|
if err := os.WriteFile(missing, []byte("version: 1\nname: miss\nsteps:\n - id: a\n request:\n method: GET\n path: /x/{{missing}}\n response:\n status: 200\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err = runParityCapture("parity:replay", "--fixtures", missing, "--target", orig.URL)
|
|
if err == nil || !strings.Contains(err.Error(), "unresolved") && !strings.Contains(err.Error(), "placeholder") {
|
|
t.Fatalf("missing placeholder: %v %s", err, out)
|
|
}
|
|
|
|
unclassified := httptest.NewServer(jsonHandler(`{"token":"` + jwt + `"}`))
|
|
t.Cleanup(unclassified.Close)
|
|
leakOut := filepath.Join(outDir, "should-not-exist.yaml")
|
|
varsForReject := filepath.Join(t.TempDir(), "reject.yaml")
|
|
out, err = runParityCapture("parity:record", "--spec", spec, "--target", unclassified.URL, "--output", leakOut, "--vars", varsForReject)
|
|
if err == nil {
|
|
t.Fatal("unclassified jwt must fail record")
|
|
}
|
|
assertNoSecrets(t, out+"\n"+err.Error(), jwt, inv)
|
|
if _, statErr := os.Stat(leakOut); !os.IsNotExist(statErr) {
|
|
t.Fatalf("unclassified jwt committed fixture: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func runParityCapture(args ...string) (string, error) {
|
|
var buf bytes.Buffer
|
|
root, err := bonfire.NewRootIO("summer", toolCommands(), bytes.NewReader(nil), &buf, &buf)
|
|
if err != nil {
|
|
return buf.String(), err
|
|
}
|
|
root.SetArgs(args)
|
|
err = root.Execute()
|
|
return buf.String(), err
|
|
}
|
|
|
|
func assertNoSecrets(t *testing.T, text string, secrets ...string) {
|
|
t.Helper()
|
|
for _, secret := range secrets {
|
|
if secret != "" && strings.Contains(text, secret) {
|
|
t.Fatalf("secret %q leaked into CLI output:\n%s", secret, text)
|
|
}
|
|
}
|
|
if strings.Contains(text, "auth_token=") && !strings.Contains(text, "{{") {
|
|
t.Fatalf("auth_token cookie leaked:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func errString(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func TestParityCommandContractUnknownCredential(t *testing.T) {
|
|
store, err := tide.OpenStore("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
step := tide.Step{
|
|
ID: "bad",
|
|
Request: tide.Request{
|
|
Method: http.MethodGet,
|
|
Path: "/x",
|
|
Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb"},
|
|
},
|
|
Response: tide.Response{Status: 200, Body: tide.Body(`{"ok":true}`)},
|
|
}
|
|
if err := tide.ScrubStep(store, &step); err == nil {
|
|
t.Fatal("unknown leftover jwt must fail")
|
|
} else if strings.Contains(err.Error(), "eyJ") {
|
|
t.Fatalf("scrub error leaked jwt: %v", err)
|
|
}
|
|
}
|