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:
@@ -77,6 +77,27 @@ for _, target := range []string{port.URL, broken.URL} {
|
||||
|
||||
The first port returns other IDs and timestamps and passes; the second changed a title and fails with the JSON path of the difference. A difference makes `tide.ReplayFlow` return a `tide.MismatchError` that carries the full `tide.Result`.
|
||||
|
||||
## Uploads
|
||||
|
||||
A step that uploads a file describes its multipart body as `parts` instead of a `body`. Each `tide.Part` is a text field with a `value`, or a file field whose bytes live in a file beside the fixture:
|
||||
|
||||
```yaml
|
||||
request:
|
||||
method: POST
|
||||
path: /api/blog/posts/{{id:post}}/photos
|
||||
parts:
|
||||
- name: caption
|
||||
value: Cover
|
||||
- name: file
|
||||
file: files/cover.png
|
||||
content_type: image/png
|
||||
sha256: <sha256 of files/cover.png>
|
||||
```
|
||||
|
||||
Recording and replaying encode the parts in order with one fixed boundary, `tide.MultipartBoundary`, so the reference backend and the port receive the same bytes. Set `BaseDir` on `tide.RecordConfig` and `tide.ReplayConfig` to the fixture directory the files are read from. `tide.LoadFlow` refuses a fixture whose part file is missing or no longer matches its `sha256`, and the file bytes are never copied into the YAML.
|
||||
|
||||
Upload responses carry URLs with random parts: the partition and disk name of the original, and the file id in a thumbnail name. Under `url` and `thumb_url` keys, the normalizer checks the WinterCMS shape below the uploads prefix (`tide.ReplayConfig` `UploadPrefix`, `tide.DefaultUploadPrefix` by default) and masks only the random parts. The prefix, the thumbnail size and mode and the extension are still compared, so a port that serves `/storage/uploads/...` or makes 100 by 100 thumbnails instead of 200 by 200 fails the diff.
|
||||
|
||||
## The parity commands
|
||||
|
||||
The `summer` CLI wraps tide. Run the reference backend and the port on loopback addresses:
|
||||
@@ -100,6 +121,6 @@ Realtime side effects are part of the contract too. `summer parity:broadcasts` r
|
||||
summer parity:broadcasts --flow testdata/broadcasts/flows/post-lifecycle.yaml --step delete --name deleted --target http://127.0.0.1:8000 --vars /tmp/parity/vars.yaml --out testdata/broadcasts/deleted.yaml
|
||||
```
|
||||
|
||||
Point the reference backend's Centrifugo API URL at the recorder (`127.0.0.1:8424` by default). `--step` keeps only the publications of one step, running the earlier steps as setup. Timestamps, the actor and captured IDs are masked (`tide.NormalizePublications`), so the Go port's publications, recorded the same way, compare with `tide.DiffPublications`. A golden with `--pending` set is recorded but not yet asserted.
|
||||
Point the reference backend's Centrifugo API URL at the recorder (`127.0.0.1:8424` by default). `--step` keeps only the publications of one step, running the earlier steps as setup. Timestamps, the actor, the `*_at` dates inside a published album and captured IDs are masked (`tide.NormalizePublications`), so the Go port's publications, recorded the same way, compare with `tide.DiffPublications`. A golden with `--pending` set is recorded but not yet asserted.
|
||||
|
||||
On the Go side, the memory realtime driver records publications the same way in tests; see [Realtime](realtime.md).
|
||||
|
||||
@@ -23,7 +23,19 @@ uploads:
|
||||
public_path_prefix: /storage/uploads
|
||||
```
|
||||
|
||||
Files are laid out as WinterCMS lays out its uploads disk, so a copy of a WinterCMS `storage/app/uploads/public` directory can serve as the bucket after a port. `public_path_prefix` is the URL prefix that file and thumbnail URLs start with.
|
||||
Files are laid out as WinterCMS lays out its uploads disk, so a copy of a WinterCMS `storage/app/uploads/public` directory can serve as the bucket after a port. `public_path_prefix` is the URL prefix that file and thumbnail URLs start with. `attach.PublicURL` builds the URL of any blob key from it, and `attach.File.URL` the URL of an original.
|
||||
|
||||
### The WinterCMS layout
|
||||
|
||||
A port whose clients already store or compare upload URLs keeps WinterCMS's URLs too. WinterCMS serves public uploads from `storage/app/uploads/public` under the URL path `/storage/app/uploads/public` (`cms.storage.uploads.path` plus `/public`). Root the bucket at that directory and use the same prefix:
|
||||
|
||||
```yaml
|
||||
uploads:
|
||||
bucket_url: file://./storage/app/uploads/public
|
||||
public_path_prefix: /storage/app/uploads/public
|
||||
```
|
||||
|
||||
An original then has the URL `/storage/app/uploads/public/<partition>/<disk_name>`, as WinterCMS's `File::getPath()` returns, and a 200 by 200 cropped thumbnail `/storage/app/uploads/public/<partition>/thumb_<id>_200_200_0_0_crop.<ext>`, as `getThumb()` returns. The framework default stays `/storage/uploads`, so only an application that sets these keys changes its URLs.
|
||||
|
||||
Application code gets the bucket with `app.Lookup[*blob.Bucket]()` and reads and writes it through the `gocloud.dev/blob` API. Model attachments, thumbnails and deleting files after commit are covered in [Attachments](../database/attachments.md).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user