fix(02-03): expand expected placeholders after replay capture
Unquoted numeric id placeholders made recorded JSON illegal to parse. Replay now recaptures, persists vars, expands the expected body, and diffs against the live response. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -272,6 +272,53 @@ func post(t *testing.T, url, session, ct, body string) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayExpandsUnquotedIDPlaceholdersAfterCapture(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/login":
|
||||
_, _ = w.Write([]byte(`{"token":"` + testJWT + `","user":{"id":1}}`))
|
||||
case "/genres":
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":1,"name":"Rock","album_count":0}]}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
flow := Flow{
|
||||
Version: 1,
|
||||
Name: "seed-then-list",
|
||||
Steps: []Step{
|
||||
{
|
||||
ID: "login",
|
||||
Request: Request{Method: http.MethodPost, Path: "/login"},
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Headers: jsonCT(),
|
||||
Body: Body(`{"token":"{{jwt:alice}}","user":{"id":{{id:alice}}}}`),
|
||||
},
|
||||
Capture: []CaptureRule{
|
||||
{From: "response.json", Path: "$.token", As: "jwt:alice", Category: "jwt"},
|
||||
{From: "response.json", Path: "$.user.id", As: "id:alice"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "genres",
|
||||
Request: Request{Method: http.MethodGet, Path: "/genres"},
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Headers: jsonCT(),
|
||||
Body: Body(`{"data":[{"id":{{id:alice}},"name":"Rock","album_count":0}]}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if _, err := ReplayFlow(context.Background(), flow, ReplayConfig{Target: srv.URL}); err != nil {
|
||||
t.Fatalf("replay with unquoted id placeholders: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubShortNumericIDsDoNotCorruptPaths(t *testing.T) {
|
||||
store, err := OpenStore("")
|
||||
if err != nil {
|
||||
|
||||
@@ -59,7 +59,18 @@ func ReplayFlow(ctx context.Context, flow Flow, cfg ReplayConfig) (Result, error
|
||||
skipRest = true
|
||||
continue
|
||||
}
|
||||
_ = ScrubStep(store, &live)
|
||||
if err := store.Save(); err != nil {
|
||||
return result, fmt.Errorf("tide: replay step %s: %w", step.ID, err)
|
||||
}
|
||||
want.Response, err = expandResponse(want.Response, store)
|
||||
if err != nil {
|
||||
sr.OK = false
|
||||
result.OK = false
|
||||
sr.Diffs = append(sr.Diffs, Diff{Path: "expected", Expected: "resolved placeholders", Actual: err.Error()})
|
||||
result.Steps = append(result.Steps, sr)
|
||||
skipRest = true
|
||||
continue
|
||||
}
|
||||
sr.Diffs = append(sr.Diffs, compareStep(want, live.Response)...)
|
||||
if len(sr.Diffs) > 0 {
|
||||
sr.OK = false
|
||||
|
||||
@@ -199,6 +199,26 @@ func expandRequest(req Request, store *Store) (Request, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func expandResponse(resp Response, store *Store) (Response, error) {
|
||||
out := resp
|
||||
var err error
|
||||
if resp.Headers != nil {
|
||||
out.Headers = make(map[string]string, len(resp.Headers))
|
||||
for k, v := range resp.Headers {
|
||||
out.Headers[k], err = store.Expand(v)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
body, err := store.Expand(string(resp.Body))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
out.Body = Body(body)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CaptureStep writes named values from the step into the store.
|
||||
func CaptureStep(store *Store, step *Step) error {
|
||||
if store == nil || step == nil || len(step.Capture) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user