Serve every blossom spec from one mock
This commit is contained in:
parent
6e6c1612fe
commit
c401cd8bd8
2 changed files with 85 additions and 103 deletions
|
|
@ -260,48 +260,101 @@ export type BlossomOptions = {
|
||||||
server: string
|
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
|
* 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.
|
* 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 {origin} = new URL(server)
|
||||||
const blobs = new Map<string, {body: Buffer; type: string}>()
|
const blobs = new Map<string, {body: Buffer; type: string}>()
|
||||||
|
|
||||||
return context.route(`${origin}/**`, route => {
|
let held = Promise.resolve()
|
||||||
const request = route.request()
|
let open = () => {}
|
||||||
const method = request.method()
|
let refusal = ""
|
||||||
const {pathname} = new URL(request.url())
|
|
||||||
|
|
||||||
if (pathname === "/upload") {
|
const handle: BlossomHandle = {
|
||||||
if (method === "HEAD") {
|
install: async context => {
|
||||||
return route.fulfill({status: 200, body: ""})
|
await context.route(`${origin}/**`, async route => {
|
||||||
}
|
const request = route.request()
|
||||||
|
const method = request.method()
|
||||||
|
const {pathname} = new URL(request.url())
|
||||||
|
|
||||||
if (method === "PUT") {
|
if (pathname === "/upload") {
|
||||||
const body = request.postDataBuffer() ?? Buffer.alloc(0)
|
// BUD-06, which the app asks before spending an upload. A refusal has to expose its
|
||||||
const type = request.headers()["content-type"] ?? "application/octet-stream"
|
// reason cross-origin or all the toast can say is the status code.
|
||||||
const sha256 = createHash("sha256").update(body).digest("hex")
|
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({
|
if (method === "PUT") {
|
||||||
json: {sha256, type, url: `${origin}/${sha256}`, size: body.length, uploaded: now()},
|
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) {
|
blobs.set(sha256, {body, type})
|
||||||
return route.fulfill({
|
|
||||||
contentType: blob.type,
|
return route.fulfill({
|
||||||
body: method === "HEAD" ? "" : blob.body,
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import {createHash} from "node:crypto"
|
import type {Locator, Page} from "@playwright/test"
|
||||||
import type {BrowserContext, Locator, Page} from "@playwright/test"
|
import {HOUR} from "@welshman/lib"
|
||||||
import {HOUR, now} from "@welshman/lib"
|
|
||||||
import {MessagingRelayList, RelayList} from "@welshman/domain"
|
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"
|
import type {SeededSpace, TestUser} from "../harness"
|
||||||
|
|
||||||
// The path the app builds for a conversation — see makeChatId in src/app/chats.ts.
|
// 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"),
|
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
|
// 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.
|
// is only loadable by somebody else once its author has one.
|
||||||
const seedRelayList = (space: SeededSpace, user: TestUser) =>
|
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 path = roomPath(scenario.space("space").url, "general")
|
||||||
const blossom = serveBlossom()
|
|
||||||
|
|
||||||
const alice = await as(users.alice, path)
|
const alice = await as(users.alice, path)
|
||||||
const bob = await as(users.bob, 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 blossom.install(bob.context())
|
||||||
|
|
||||||
await expect(timeline(alice).getByText("morning all")).toBeVisible()
|
await expect(timeline(alice).getByText("morning all")).toBeVisible()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue