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