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

@@ -42,6 +42,9 @@ type Request struct {
Query string `yaml:"query,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
Body Body `yaml:"body,omitempty"`
// Parts is a multipart/form-data body, encoded with MultipartBoundary
// when the step runs. A request has Body or Parts, never both.
Parts []Part `yaml:"parts,omitempty"`
}
// Response is the recorded or expected HTTP reply.
@@ -73,21 +76,28 @@ type NormalizeRule struct {
type Body string
// RecordConfig injects the HTTP target, client and body bound for recording.
// BaseDir is the fixture directory that request part files are read from.
type RecordConfig struct {
Target string
Client *http.Client
MaxBody int64
Store *Store
Rules Rules
BaseDir string
}
// ReplayConfig injects the HTTP target, client and body bound for replay.
// BaseDir is the fixture directory that body_file sidecars and request part
// files are read from.
// UploadPrefix is the public URL prefix of uploaded files whose url and
// thumb_url values are compared by shape; empty means DefaultUploadPrefix.
type ReplayConfig struct {
Target string
Client *http.Client
MaxBody int64
Store *Store
BaseDir string
Target string
Client *http.Client
MaxBody int64
Store *Store
BaseDir string
UploadPrefix string
}
// Result is the outcome of replaying a flow.
@@ -169,6 +179,9 @@ func validateFlow(flow Flow) error {
if err := validateSidecar(step.Response.BodyFile); err != nil {
return fmt.Errorf("tide: step %s: %w", step.ID, err)
}
if err := validateParts(step.Request); err != nil {
return fmt.Errorf("tide: step %s: %w", step.ID, err)
}
}
return nil
}