test(relay): give every media e2e upload its own blob hash (#9272)

## Summary
Split out of the `flakey-monday` flake-fix series. Two Relay E2E media
tests shared one deterministic blob hash across every test in their
binaries. In enforce mode (`BUZZ_MEDIA_READ_MODE=enforce`, on for the
Relay E2E lane since #9077), a rejected publish of that shared hash left
a sticky publication marker that made ranged GETs from the other tests
return 404. Every media e2e upload now has its own blob hash.

Commits (cherry-picked from `flakey-monday`, original messages kept):
- `test(media): give each video e2e test its own MP4 blob`
- `test(media): publish images of their own in the media e2e tests`

### Root cause and fix per test

**`test_video_range_request_206` / `test_video_range_request_416`**
(`crates/buzz-test-client/tests/e2e_media_video.rs`): a ranged GET
returned 404 instead of 206/416 in 9 of 55 Relay E2E jobs.
- Root cause (confirmed): `build_test_mp4()` was deterministic, so every
test in the binary uploaded the same sha256, and the tests run in
parallel against one relay. In enforce mode, `validate_media_references`
records a sticky `media_publications` marker for every hash an event
publishes, before dispatch, so a later rejection does not undo it.
`test_video_poster_imeta_rejects_video_as_poster` published the shared
hash and was then rejected by imeta validation. From then on
`media_access` gave an uploader with no readable reference `Restricted`,
served as 404, until the accepted poster test stored its event in an
open channel.
- Fix: `build_test_mp4()` appends a top-level `free` box holding a
random UUID, so each call yields a distinct, still valid fast-start MP4.
The upload validator allows `free` and does not inspect its payload. The
relay is unchanged: the pre-dispatch marker is the documented
fail-closed contract in `docs/operator-private-media.md`.

**`test_video_poster_imeta_accepted_via_ws`** and
**`e2e_media_extended::test_ws_valid_imeta`**: hardening, not a fix for
an observed failure. Both published a fixed 1x1 JPEG byte-identical to
`tiny_jpeg()` in `e2e_media.rs`, which `test_upload_and_get` reads back
through an uploader preview. That currently passes (a readable live
reference wins before the marker matters), but a publishing test that
shares a fixed blob with other media tests is one rejected or private
event away from the same flake. Both now publish an 8x8 PNG of random
pixels from a new `unique_png()` helper; `test_ws_valid_imeta`'s imeta
declares `image/png` and the real size; `tiny_jpeg()` is removed from
`e2e_media_video.rs`.

### Related issue
N/A (flake fixes; none found).

### Testing
Human testing is still required. This PR does not carry
`buzz-review-completed`; the author has not yet run these tests
themselves on this branch.

#### Verification
These changes were verified locally on the `flakey-monday` branch before
the split, against an enforce-mode relay with docker Postgres/MinIO:
- Old code, fresh enforce-mode relay: reject test then range tests
failed both range tests with 404 at the CI lines (reproduction).
- Whole `e2e_media_video` binary, `--test-threads=8`: 20/20 passed.
- Reject test then range tests against the polluted hash: 3/3 passed.
- The CI "Media read-auth e2e" command (`e2e_media`,
`e2e_media_extended`, `e2e_media_video`, `--ignored`), 5 back-to-back
runs on the same scratch database (the persistent-relay case): 5/5 runs,
40/40 tests each time.
- `cargo fmt --check` and `clippy --tests`: clean.

Nothing was re-run on this split branch: the workstation was overloaded,
so no tests, builds or linters were executed after cherry-picking. CI on
this PR is the verification for the split branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: mtoohey <mtoohey@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Matt Toohey
2026-11-10 05:56:58 +11:00
committed by GitHub
parent 838976d6e4
commit b082b1464c
2 changed files with 46 additions and 41 deletions

View File

@@ -127,6 +127,19 @@ fn tiny_jpeg() -> Vec<u8> {
]
}
/// An 8x8 PNG of random pixels, so every call has its own blob hash.
/// Publishing a hash leaves a sticky publication marker. Until the hash has a
/// readable reference (never, if the event is rejected), the relay refuses
/// uploader previews of it. A blob of its own keeps a publishing test from
/// reaching the fixed blobs, like `tiny_jpeg`, that other media tests read.
fn unique_png() -> Vec<u8> {
let mut png = Vec::new();
image::RgbImage::from_fn(8, 8, |_, _| image::Rgb(rand::random()))
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.expect("encode PNG");
png
}
fn tiny_png() -> Vec<u8> {
// Valid 2x2 red PNG generated by ffmpeg, with ffmpeg's pHYs chunk stripped:
// `validate_png_metadata_free` rejects pHYs as an identity channel, so the
@@ -658,10 +671,10 @@ async fn test_ws_valid_imeta() {
let channel_id = channel_uuid.to_string();
println!("Channel: {channel_id}");
// Upload a JPEG to get a valid sha256
let jpeg = tiny_jpeg();
let sha256 = hex::encode(Sha256::digest(&jpeg));
let resp = upload(&http, &keys, &jpeg).await;
// Upload an image of its own to get a valid sha256
let png = unique_png();
let sha256 = hex::encode(Sha256::digest(&png));
let resp = upload(&http, &keys, &png).await;
assert_eq!(resp.status(), 200);
// Connect via WebSocket
@@ -675,10 +688,10 @@ async fn test_ws_valid_imeta() {
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse([
"imeta",
&format!("url http://localhost:3000/media/{sha256}.jpg"),
"m image/jpeg",
&format!("url http://localhost:3000/media/{sha256}.png"),
"m image/png",
&format!("x {sha256}"),
"size 347",
&format!("size {}", png.len()),
])
.unwrap(),
])

View File

@@ -79,10 +79,18 @@ fn blossom_auth_header(event: &nostr::Event) -> String {
)
}
/// Build a minimal but structurally valid fast-start MP4 (H.264, 1s, 320×240).
/// Build a minimal but structurally valid fast-start MP4 (H.264, 1s, 320×240)
/// that is unique to this call.
///
/// Layout: ftyp | moov(mvhd + trak(tkhd + mdia(mdhd + hdlr + minf(vmhd + dinf + stbl)))) | mdat
/// Layout: ftyp | moov(mvhd + trak(tkhd + mdia(mdhd + hdlr + minf(vmhd + dinf + stbl)))) | mdat | free
/// This is enough for `infer` to detect video/mp4 and for the `mp4` crate to parse.
///
/// The trailing `free` box carries a random nonce so every test uploads its own
/// blob. Tests run in parallel against one relay, and under
/// `BUZZ_MEDIA_READ_MODE=enforce` any event that publishes a hash records a
/// sticky publication marker, even if the event is later rejected. That marker
/// ends uploader-preview reads of the hash, so a shared blob let one test's
/// publish turn another test's GET into a 404.
fn build_test_mp4() -> Vec<u8> {
fn box_wrap(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (8 + payload.len()) as u32;
@@ -263,8 +271,9 @@ fn build_test_mp4() -> Vec<u8> {
let moov_payload = [&mvhd[..], &trak].concat();
let moov = box_wrap(b"moov", &moov_payload);
let mdat = box_wrap(b"mdat", &[]);
let nonce = box_wrap(b"free", uuid::Uuid::new_v4().as_bytes());
[ftyp, moov, mdat].concat()
[ftyp, moov, mdat, nonce].concat()
}
/// Upload a valid MP4 video via Blossom, verify the BlobDescriptor includes
@@ -481,34 +490,17 @@ fn relay_ws_url() -> String {
.replace("https://", "wss://")
}
/// Minimal valid JPEG (1x1 pixel) — used as a poster frame blob.
fn tiny_jpeg() -> Vec<u8> {
vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06,
0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B,
0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, 0x2C, 0x30, 0x31,
0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF,
0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00,
0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05,
0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,
0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A,
0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56,
0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93,
0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9,
0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6,
0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x7B, 0x94,
0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xD9,
]
/// An 8x8 PNG of random pixels, so every call has its own blob hash.
/// Publishing a hash leaves a sticky publication marker. Until the hash has a
/// readable reference (never, if the event is rejected), the relay refuses
/// uploader previews of it. A blob of its own keeps a publishing test from
/// reaching the fixed blobs, like `tiny_jpeg`, that other media tests read.
fn unique_png() -> Vec<u8> {
let mut png = Vec::new();
image::RgbImage::from_fn(8, 8, |_, _| image::Rgb(rand::random()))
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.expect("encode PNG");
png
}
/// Upload a video + poster, then send a message with imeta `image` field
@@ -561,8 +553,8 @@ async fn test_video_poster_imeta_accepted_via_ws() {
let video_desc: serde_json::Value = video_resp.json().await.unwrap();
let video_size = video_desc["size"].as_u64().unwrap();
// 3. Upload poster (tiny JPEG)
let poster = tiny_jpeg();
// 3. Upload poster (unique PNG)
let poster = unique_png();
let poster_sha = hex::encode(Sha256::digest(&poster));
let poster_auth = sign_blossom_auth(&keys, &poster_sha);
let poster_resp = client
@@ -593,7 +585,7 @@ async fn test_video_poster_imeta_accepted_via_ws() {
"m video/mp4",
&format!("x {video_sha}"),
&format!("size {video_size}"),
&format!("image {base}/media/{poster_sha}.jpg"),
&format!("image {base}/media/{poster_sha}.png"),
])
.unwrap(),
])