Files
summercms/cmd/summer/parity_contract_test.go
Jakub Zych 5e50b166ef refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
2026-09-28 02:21:02 +02:00

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/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/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)
}
}