Files
summercms/modules/tide/replay.go
Jakub Zych e06e0cc8bf feat(12-01): record multipart uploads and match Winter upload URLs
- attach.PublicURL and (*File).URL build Winter File::getPath() URLs; the
  thumbnailer decodes webp via golang.org/x/image v0.46.0 and checks the
  image size from the header before decoding
- tide requests carry multipart parts (files beside the fixture pinned by
  sha256) encoded with the fixed MultipartBoundary, so PHP and Go receive
  byte-identical bodies
- tide masks the random partition, disk name and file id of url/thumb_url
  upload URLs while still diffing prefix, size, mode and extension, and
  NormalizePublications masks Carbon dates in the published album
2026-10-02 11:33:42 +02:00

113 lines
3.3 KiB
Go

package tide
import (
"context"
"fmt"
"strings"
)
// ReplayFlow executes each recorded step against target and diffs responses.
func ReplayFlow(ctx context.Context, flow Flow, cfg ReplayConfig) (Result, error) {
if err := validateFlow(flow); err != nil {
return Result{}, err
}
if strings.TrimSpace(cfg.Target) == "" {
return Result{}, fmt.Errorf("tide: replay target is required")
}
client := cfg.Client
if client == nil {
client = defaultClient()
}
limit := maxBody(cfg.MaxBody)
store := cfg.Store
if store == nil {
store = mustMemoryStore()
}
result := Result{OK: true, Steps: make([]StepResult, 0, len(flow.Steps))}
skipRest := false
for _, step := range flow.Steps {
if skipRest {
result.Steps = append(result.Steps, StepResult{ID: step.ID, Skipped: true})
continue
}
sr := StepResult{ID: step.ID, OK: true}
want := step
if err := materializeSidecar(cfg.BaseDir, &want.Response); err != nil {
return result, fmt.Errorf("tide: replay step %s: %w", step.ID, err)
}
req, err := expandRequest(step.Request, store)
if err != nil {
sr.OK = false
result.OK = false
sr.Diffs = append(sr.Diffs, Diff{Path: "request", Expected: "resolved placeholders", Actual: err.Error()})
result.Steps = append(result.Steps, sr)
skipRest = true
continue
}
req, err = prepareRequest(req, cfg.BaseDir)
if err != nil {
return result, fmt.Errorf("tide: replay step %s: %w", step.ID, err)
}
got, err := doStep(ctx, client, cfg.Target, req, limit)
if err != nil {
return result, fmt.Errorf("tide: replay step %s: %w", step.ID, err)
}
live := step
live.Request = req
live.Response = got
if err := CaptureStep(store, &live); err != nil {
sr.OK = false
result.OK = false
sr.Diffs = append(sr.Diffs, Diff{Path: "capture", Expected: "captured value", Actual: err.Error()})
result.Steps = append(result.Steps, sr)
skipRest = true
continue
}
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, compareStepWith(want, live.Response, maskOptions{uploadPrefix: cfg.UploadPrefix})...)
if len(sr.Diffs) > 0 {
sr.OK = false
result.OK = false
}
result.Steps = append(result.Steps, sr)
}
if !result.OK {
return result, &MismatchError{Result: result}
}
return result, nil
}
func mustMemoryStore() *Store {
s, _ := OpenStore("")
return s
}
func compareStep(want Step, got Response) []Diff {
return compareStepWith(want, got, maskOptions{})
}
func compareStepWith(want Step, got Response, opts maskOptions) []Diff {
var diffs []Diff
if want.Response.Status != 0 && got.Status != want.Response.Status {
diffs = append(diffs, Diff{
Path: "status",
Expected: fmt.Sprintf("%d", want.Response.Status),
Actual: fmt.Sprintf("%d", got.Status),
})
}
diffs = append(diffs, compareHeaders(want.Response.Headers, got.Headers, want.Headers)...)
diffs = append(diffs, compareBodiesWith(want.Response, got, want, opts)...)
return diffs
}