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
This commit is contained in:
Jakub Zych
2026-10-02 11:33:42 +02:00
parent f9b7f2ea33
commit e06e0cc8bf
19 changed files with 870 additions and 34 deletions

View File

@@ -207,6 +207,19 @@ func expandRequest(req Request, store *Store) (Request, error) {
return Request{}, err
}
out.Body = Body(body)
if len(req.Parts) > 0 {
// Variables expand in part values only, never in file bytes.
out.Parts = make([]Part, len(req.Parts))
for i, p := range req.Parts {
out.Parts[i] = p
if p.File == "" {
out.Parts[i].Value, err = store.Expand(p.Value)
if err != nil {
return Request{}, err
}
}
}
}
return out, nil
}
@@ -461,6 +474,14 @@ func ScrubStep(store *Store, step *Step) error {
step.Request.Query = replaceAll(step.Request.Query, pairs, true)
step.Request.Headers = scrubMap(step.Request.Headers, pairs, true)
step.Request.Body = Body(replaceAll(string(step.Request.Body), pairs, false))
if len(step.Request.Parts) > 0 {
parts := make([]Part, len(step.Request.Parts))
for i, p := range step.Request.Parts {
parts[i] = p
parts[i].Value = replaceAll(p.Value, pairs, false)
}
step.Request.Parts = parts
}
for _, rule := range step.Capture {
if strings.TrimSpace(rule.From) != "request.form" {
continue
@@ -606,6 +627,11 @@ func rejectUnclassifiedCredentials(step Step) error {
if err := check("request body", string(step.Request.Body)); err != nil {
return err
}
for _, p := range step.Request.Parts {
if err := check("request part "+p.Name, p.Value); err != nil {
return err
}
}
for k, v := range step.Response.Headers {
if err := check("response header "+k, v); err != nil {
return err