Serve every blossom spec from one mock

This commit is contained in:
Coracle-Bot 2026-09-10 03:24:06 +00:00 committed by hodlbod
parent 6e6c1612fe
commit c401cd8bd8
2 changed files with 85 additions and 103 deletions

View file

@ -260,48 +260,101 @@ export type BlossomOptions = {
server: string
}
/**
* What a spec can turn on. Each of these is off by default, so a scenario that only needs somewhere
* for an upload to land ignores all of it.
*/
export type BlossomHandle = {
// The same server, blobs included, in another user's context. A conversation's image is uploaded
// encrypted and decrypted by the recipient, so the bytes one end puts in have to be the bytes the
// other reads back.
install(context: BrowserContext): Promise<void>
// Holds every upload open until `release`, which makes the in-flight state a fact rather than a
// race against a mock that answers in a microtask.
hold(): void
release(): void
// Refuses every upload probe from here on, with a reason the app has to show.
refuse(reason: string): void
}
/**
* A blossom server that keeps what it was given. An upload is hashed exactly as the real thing
* would be, so the descriptor it answers with points at a blob this mock can then serve back.
*/
export const mockBlossom = (context: BrowserContext, {server}: BlossomOptions) => {
export const mockBlossom = async (context: BrowserContext, {server}: BlossomOptions) => {
const {origin} = new URL(server)
const blobs = new Map<string, {body: Buffer; type: string}>()
return context.route(`${origin}/**`, route => {
const request = route.request()
const method = request.method()
const {pathname} = new URL(request.url())
let held = Promise.resolve()
let open = () => {}
let refusal = ""
if (pathname === "/upload") {
if (method === "HEAD") {
return route.fulfill({status: 200, body: ""})
}
const handle: BlossomHandle = {
install: async context => {
await context.route(`${origin}/**`, async route => {
const request = route.request()
const method = request.method()
const {pathname} = new URL(request.url())
if (method === "PUT") {
const body = request.postDataBuffer() ?? Buffer.alloc(0)
const type = request.headers()["content-type"] ?? "application/octet-stream"
const sha256 = createHash("sha256").update(body).digest("hex")
if (pathname === "/upload") {
// BUD-06, which the app asks before spending an upload. A refusal has to expose its
// reason cross-origin or all the toast can say is the status code.
if (method === "HEAD") {
if (refusal) {
return route.fulfill({
status: 400,
headers: {
"X-Reason": refusal,
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "*",
},
})
}
blobs.set(sha256, {body, type})
return route.fulfill({status: 200, body: ""})
}
return route.fulfill({
json: {sha256, type, url: `${origin}/${sha256}`, size: body.length, uploaded: now()},
})
}
}
if (method === "PUT") {
await held
const blob = blobs.get(pathname.slice(1).split(".")[0])
const body = request.postDataBuffer() ?? Buffer.alloc(0)
const type = request.headers()["content-type"] ?? "application/octet-stream"
const sha256 = createHash("sha256").update(body).digest("hex")
if (blob) {
return route.fulfill({
contentType: blob.type,
body: method === "HEAD" ? "" : blob.body,
blobs.set(sha256, {body, type})
return route.fulfill({
json: {sha256, type, url: `${origin}/${sha256}`, size: body.length, uploaded: now()},
})
}
}
const blob = blobs.get(pathname.slice(1).split(".")[0])
if (blob) {
return route.fulfill({
contentType: blob.type,
body: method === "HEAD" ? "" : blob.body,
})
}
return route.fallback()
})
}
},
hold: () => {
held = new Promise<void>(resolve => {
open = resolve
})
},
release: () => open(),
refuse: reason => {
refusal = reason
},
}
return route.fallback()
})
await handle.install(context)
return handle
}
/**

View file

@ -1,8 +1,7 @@
import {createHash} from "node:crypto"
import type {BrowserContext, Locator, Page} from "@playwright/test"
import {HOUR, now} from "@welshman/lib"
import type {Locator, Page} from "@playwright/test"
import {HOUR} from "@welshman/lib"
import {MessagingRelayList, RelayList} from "@welshman/domain"
import {expect, makeTestUser, roomPath, test, users} from "../harness"
import {expect, makeTestUser, mockBlossom, roomPath, test, users} from "../harness"
import type {SeededSpace, TestUser} from "../harness"
// The path the app builds for a conversation — see makeChatId in src/app/chats.ts.
@ -22,76 +21,6 @@ const gifFile = (name: string) => ({
buffer: Buffer.from(GIF, "base64"),
})
// A blossom server whose blobs are shared by every context in a test, and whose next upload can be
// held open. The harness's own `mockBlossom` keeps blobs per browser context, which a conversation's
// image cannot use: it is uploaded encrypted and decrypted by the recipient, so the bytes alice puts
// in have to be the bytes bob reads back. Holding the upload makes the in-flight state a fact rather
// than a race against a mock that answers in a microtask.
const serveBlossom = () => {
const blobs = new Map<string, {body: Buffer; type: string}>()
let held = Promise.resolve()
let open = () => {}
let refusal = ""
return {
hold: () => {
held = new Promise<void>(resolve => {
open = resolve
})
},
release: () => open(),
refuse: (reason: string) => {
refusal = reason
},
install: (context: BrowserContext) =>
context.route(`${BLOSSOM_ORIGIN}/**`, async route => {
const request = route.request()
const method = request.method()
const {pathname} = new URL(request.url())
// BUD-06, which the app asks before spending an upload. A refusal's reason has to be
// exposed cross-origin or all the toast can say is the status code.
if (pathname === "/upload" && method === "HEAD") {
if (refusal) {
return route.fulfill({
status: 400,
headers: {
"X-Reason": refusal,
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "*",
},
})
}
return route.fulfill({status: 200, body: ""})
}
if (pathname === "/upload" && method === "PUT") {
await held
const body = request.postDataBuffer() ?? Buffer.alloc(0)
const type = request.headers()["content-type"] ?? "application/octet-stream"
const sha256 = createHash("sha256").update(body).digest("hex")
blobs.set(sha256, {body, type})
const url = `${BLOSSOM_ORIGIN}/${sha256}`
return route.fulfill({json: {sha256, type, url, size: body.length, uploaded: now()}})
}
const blob = blobs.get(pathname.slice(1).split(".")[0])
if (blob) {
return route.fulfill({contentType: blob.type, body: method === "HEAD" ? "" : blob.body})
}
return route.fallback()
}),
}
}
// Outbox routing resolves everything about a person through their relay list, so a seeded profile
// is only loadable by somebody else once its author has one.
const seedRelayList = (space: SeededSpace, user: TestUser) =>
@ -304,12 +233,12 @@ test("US-057 attach and send an image", async ({seed, as}) => {
})
const path = roomPath(scenario.space("space").url, "general")
const blossom = serveBlossom()
const alice = await as(users.alice, path)
const bob = await as(users.bob, path)
await blossom.install(alice.context())
const blossom = await mockBlossom(alice.context(), {server: BLOSSOM_ORIGIN})
await blossom.install(bob.context())
await expect(timeline(alice).getByText("morning all")).toBeVisible()