Files
tech-log-frontend/tests/browser-capabilities/resumable-upload.spec.ts
T
DongHyeonkaandClaude Opus 5 bdee07a93b chore: sync the frontend template from a0fbafb to 5434760
Carries eight template commits: the provider sandbox actually running, release
admission to a named environment, the product feature manifest with its runtime
kill switch, architecture and documentation rules that match what is enforced,
the removability fixtures, and the browser, visual and performance evidence.

Product identity is unchanged. `package.json` keeps `tech-log-frontend` and the
catalog keeps the Tech Log naming; the home page was not in the delta. The
visual baselines are this product's own — the template's were excluded from the
transplant and these were regenerated here, where the only difference is the
platform overview's new product-feature section.

What this repository gains operationally: `config/runtime/{local,development,
staging,production}.json` with `FE-GATE-027` refusing an artifact whose runtime
document does not match the environment it is being admitted to, and
`FEATURE_OVERRIDES` for taking an installed feature out of service without a
rebuild.

Verified here: eight gates green, build green, visual 5/5, and 1,858 of 1,859
tests in the suites that do not need a sandbox — the one failure passes in
isolation and is a jsdom lazy-chunk timeout under parallel load. The provider
suites cannot run on this machine at all: `kernel.apparmor_restrict_unprivileged
_userns=1` makes `bwrap --unshare-net` fail, reproducible without any code from
either repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 21:34:19 +09:00

527 lines
17 KiB
TypeScript

import { createHash } from "node:crypto";
import type { Route } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.ts";
const PAGE_ORIGIN = "http://127.0.0.1:4174";
const API_ORIGIN = "https://upload-api.example.test";
const OBJECT_ORIGIN = "https://upload-objects.example.test";
const SESSION_ID = "session_browser_01";
const RESOURCE_ID = "resource_browser_01";
const UPLOAD_PROTOCOL = "PRESIGNED_MULTIPART_V1";
type JsonRecord = Readonly<Record<string, unknown>>;
function corsHeaders(): Record<string, string> {
return {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "content-type",
"access-control-allow-methods": "POST, OPTIONS",
"access-control-allow-origin": PAGE_ORIGIN,
};
}
async function fulfillJson(
route: Route,
status: number,
value: unknown,
): Promise<void> {
const body = JSON.stringify(value);
await route.fulfill({
status,
body,
headers: {
...corsHeaders(),
"content-length": String(Buffer.byteLength(body)),
"content-type": "application/json; charset=utf-8",
},
});
}
test("uploads three presigned parts through native IndexedDB, Web Locks and fetch", async ({
page,
}) => {
let session: JsonRecord | null = null;
const acceptedParts = new Map<number, JsonRecord>();
const issuedParts = new Map<number, JsonRecord>();
const controlRequests: Readonly<{
path: string;
body: JsonRecord;
}>[] = [];
const uploadedBodies: Readonly<{
partNumber: number;
bytes: number[];
checksum: string | undefined;
}>[] = [];
let completedParts: readonly JsonRecord[] = [];
await page.route(`${API_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({ status: 204, headers: corsHeaders() });
return;
}
const path = new URL(route.request().url()).pathname;
const body = route.request().postDataJSON() as JsonRecord;
controlRequests.push({ path, body });
if (path === "/uploads/create") {
const fingerprint = body.fingerprint as JsonRecord;
session = {
protocol: UPLOAD_PROTOCOL,
sessionId: SESSION_ID,
requestBindingSha256: body.requestBindingSha256,
fingerprint,
partSizeBytes: body.requestedPartSizeBytes,
partCount: fingerprint.partCount,
maxConcurrency: 1,
expiresAtEpochMs: Date.now() + 5 * 60_000,
};
await fulfillJson(route, 201, session);
return;
}
if (path === "/uploads/status") {
if (!session) {
await fulfillJson(route, 404, { state: "NOT_FOUND" });
return;
}
await fulfillJson(route, 200, {
state: "ACTIVE",
session,
acceptedParts: [...acceptedParts.values()].sort(
(left, right) =>
Number(left.partNumber) - Number(right.partNumber),
),
});
return;
}
if (path === "/uploads/complete") {
completedParts = body.orderedParts as readonly JsonRecord[];
await fulfillJson(route, 200, {
state: "QUARANTINED",
protocol: UPLOAD_PROTOCOL,
sessionId: body.sessionId,
requestBindingSha256: body.requestBindingSha256,
fingerprint: body.fingerprint,
resourceId: RESOURCE_ID,
});
return;
}
if (path === "/uploads/abort") {
await fulfillJson(route, 200, { state: "ABORTED" });
return;
}
if (path === "/uploads/part-capability") {
const binding = body.binding as JsonRecord;
const partNumber = Number(binding.partNumber);
const descriptor = {
partNumber,
offset: binding.offset,
byteLength: body.byteLength,
checksumSha256: body.expectedSha256,
};
issuedParts.set(partNumber, descriptor);
const objectPath =
`/uploads/${SESSION_ID}/parts/${String(partNumber)}`;
await fulfillJson(route, 200, {
// The part capability travels in a presigned envelope, so it names the
// transfer protocol even though its binding names the upload one.
// Without it every part was refused before any object PUT was made.
protocol: "PRESIGNED_TRANSFER_V1",
capabilityReceipt: `browser-part-capability-${String(partNumber)}`,
method: "PUT",
binding,
href:
`${OBJECT_ORIGIN}${objectPath}?sig=opaque-${String(partNumber)}`,
origin: OBJECT_ORIGIN,
path: objectPath,
allowedQueryParameters: ["sig"],
requestHeaders: [
{
name: "content-type",
value: body.mediaType,
},
{
name: "x-checksum-sha256",
value: body.expectedSha256,
},
],
requiredResponseHeaders: [
{
name: "x-policy-version",
value: "v1",
},
],
digestRequestHeader: "x-checksum-sha256",
digestResponseHeader: null,
receiptResponseHeader: "etag",
expectedResponseByteLength: 0,
expectedStatus: 204,
mediaType: body.mediaType,
byteLength: body.byteLength,
maxBytes: body.byteLength,
expectedSha256: body.expectedSha256,
expiresAtEpochMs:
Number(session?.expiresAtEpochMs) - 10_000,
singleUse: true,
});
return;
}
await route.fulfill({ status: 404, headers: corsHeaders() });
});
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
"access-control-allow-headers":
"content-type, x-checksum-sha256",
"access-control-allow-methods": "PUT, OPTIONS",
"access-control-allow-origin": "*",
},
});
return;
}
const match = new URL(route.request().url()).pathname.match(
/\/parts\/([1-9][0-9]*)$/u,
);
const partNumber = Number(match?.[1]);
const body = route.request().postDataBuffer();
if (!body || !Number.isSafeInteger(partNumber)) {
await route.fulfill({ status: 400 });
return;
}
const checksum = route.request().headers()["x-checksum-sha256"];
const actualChecksum = createHash("sha256")
.update(body)
.digest("hex");
const descriptor = issuedParts.get(partNumber);
if (
!descriptor ||
checksum !== actualChecksum ||
descriptor.checksumSha256 !== actualChecksum ||
descriptor.byteLength !== body.byteLength
) {
await route.fulfill({ status: 422 });
return;
}
const receiptToken = `etag-part-${String(partNumber)}`;
acceptedParts.set(partNumber, {
...descriptor,
receiptToken,
});
uploadedBodies.push({
partNumber,
bytes: [...body],
checksum,
});
await route.fulfill({
status: 204,
headers: {
"access-control-allow-origin": "*",
"access-control-expose-headers": "etag, x-policy-version",
etag: `"${receiptToken}"`,
"x-policy-version": "v1",
},
});
});
await page.goto("/");
const result = await page.evaluate(
async ({ apiOrigin, objectOrigin }) => {
const vaultModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
const providerModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
const executorModulePath =
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
const fetchTransportModulePath =
"/src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts";
const controlPlaneModulePath =
"/src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts";
const checkpointModulePath =
"/src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts";
const partExecutorModulePath =
"/src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts";
const uploadRuntimeModulePath =
"/src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts";
const cancellationModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts";
const lockModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts";
const [
{
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
},
{ createPresignedCapabilityHttpProvider },
{ createPresignedTransferExecutor },
{ createResumableUploadFetchJsonTransport },
{ createResumableUploadHttpControlPlane },
{ createIndexedDbResumableUploadCheckpointRuntime },
{ createPresignedUploadPartExecutor },
{ createResumableUploadRuntime },
{ createBrowserUploadCancellationChannel },
{ createResumableUploadWebLock },
] = await Promise.all([
import(/* @vite-ignore */ vaultModulePath),
import(/* @vite-ignore */ providerModulePath),
import(/* @vite-ignore */ executorModulePath),
import(/* @vite-ignore */ fetchTransportModulePath),
import(/* @vite-ignore */ controlPlaneModulePath),
import(/* @vite-ignore */ checkpointModulePath),
import(/* @vite-ignore */ partExecutorModulePath),
import(/* @vite-ignore */ uploadRuntimeModulePath),
import(/* @vite-ignore */ cancellationModulePath),
import(/* @vite-ignore */ lockModulePath),
]);
const suffix = crypto.randomUUID().replaceAll("-", "");
const uploadKey = `upload_browser_${suffix}`;
const checkpointRuntime =
createIndexedDbResumableUploadCheckpointRuntime({
scope: {
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
},
blockedTimeoutMs: 2_000,
});
const vault = createPresignedCapabilityVault({
maxActiveCapabilities: 8,
});
const capabilityProvider =
createPresignedCapabilityHttpProvider({
endpoint: `${apiOrigin}/uploads/part-capability`,
vault,
allowedDataOrigins: [objectOrigin],
allowedDataPathPrefixes: ["/uploads/"],
allowedQueryParameters: ["sig"],
allowedRequestHeaders: [
"content-type",
"x-checksum-sha256",
],
allowedResponseHeaders: ["etag", "x-policy-version"],
hardMaxTransferBytes: 8,
hardMaxUploadResponseBytes: 1_024,
maxCapabilityTtlMs: 10 * 60_000,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
controlPlaneCredentials: "include",
});
const transferExecutor = createPresignedTransferExecutor({
vault,
replayGuard: createSingleUsePresignedReplayGuard(),
hardMaxTransferBytes: 8,
hardMaxChunkBytes: 2,
hardMaxUploadResponseBytes: 1_024,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
});
const transport = createResumableUploadFetchJsonTransport({
endpoints: {
CREATE_SESSION: `${apiOrigin}/uploads/create`,
GET_STATUS: `${apiOrigin}/uploads/status`,
COMPLETE: `${apiOrigin}/uploads/complete`,
ABORT: `${apiOrigin}/uploads/abort`,
},
allowedOrigins: [apiOrigin],
credentials: "include",
timeoutMs: 5_000,
maxRequestBytes: 64 * 1024,
maxResponseBytes: 64 * 1024,
maxRetryAfterMs: 1_000,
});
const controlPlane = createResumableUploadHttpControlPlane({
transport,
partCapabilities: capabilityProvider,
});
const cancellationChannelName =
`browser-upload-cancel-${suffix}`;
const crossContextCancellation =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const cancellationPeer =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const runtime = createResumableUploadRuntime({
controlPlane,
partExecutor: createPresignedUploadPartExecutor(
transferExecutor.uploadParts,
),
checkpoints: checkpointRuntime.store,
mutationLock: createResumableUploadWebLock(
navigator.locks,
"browser-resumable-upload",
),
...(crossContextCancellation
? { crossContextCancellation }
: {}),
crypto,
policy: {
partSizeBytes: 2,
maxFileBytes: 6,
maxPartCount: 3,
maxConcurrency: 2,
maxInFlightBytes: 16,
partBufferCopyFactor: 4,
maxSourceChunkBytes: 2,
maxRetries: 1,
retryBaseDelayMs: 1,
retryMaxDelayMs: 5,
maxRetryAfterMs: 1_000,
capabilityRefreshSkewMs: 1_000,
maxSessionLifetimeMs: 10 * 60_000,
providerAttemptTimeoutMs: 10_000,
},
});
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
const progress: string[] = [];
let upload: unknown;
let checkpoint: unknown;
let deletion: unknown;
let cancellationDelivered = false;
try {
if (crossContextCancellation && cancellationPeer) {
cancellationDelivered = await new Promise<boolean>(
(resolve) => {
const timer = setTimeout(() => resolve(false), 2_000);
const release = cancellationPeer.subscribe((key: string) => {
if (key !== `upload_probe_${suffix}`) return;
clearTimeout(timer);
release();
resolve(true);
});
if (
!crossContextCancellation.publish(
`upload_probe_${suffix}`,
)
) {
clearTimeout(timer);
release();
resolve(false);
}
},
);
}
upload = await runtime.upload({
uploadKey,
purpose: "browser_attachment",
mediaType: "application/octet-stream",
source: {
kind: "RANGE_READER",
reader: {
byteLength: bytes.byteLength,
async readRange(input: {
offset: number;
length: number;
signal: AbortSignal;
}) {
return input.signal.aborted
? {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "FILE_READ" as const,
retryable: false,
recovery: "NONE" as const,
},
}
: {
ok: true as const,
value: bytes.slice(
input.offset,
input.offset + input.length,
),
};
},
},
},
signal: new AbortController().signal,
onProgress(value: { phase: string }) {
progress.push(value.phase);
},
});
checkpoint = await checkpointRuntime.store.read(uploadKey);
} finally {
runtime.close();
cancellationPeer?.close();
deletion =
await checkpointRuntime.admin.deletePartition();
vault.dispose();
}
return {
upload,
checkpoint,
deletion,
progress,
webLocksAvailable: Boolean(navigator.locks),
broadcastCancellationAvailable:
Boolean(crossContextCancellation),
cancellationDelivered,
};
},
{ apiOrigin: API_ORIGIN, objectOrigin: OBJECT_ORIGIN },
);
expect(result.webLocksAvailable).toBe(true);
expect(result.broadcastCancellationAvailable).toBe(true);
expect(result.cancellationDelivered).toBe(true);
expect(result.upload).toMatchObject({
ok: true,
value: {
state: "QUARANTINED",
resourceId: RESOURCE_ID,
byteLength: 5,
replayed: false,
},
});
expect(result.checkpoint).toEqual({ ok: true, value: null });
// The deletion reports the physical effect it observed, not only the state it
// reached, so a caller can tell a delete that happened from one that found
// nothing to do. The fixture asserts it rather than ignoring it.
expect(result.deletion).toEqual({
ok: true,
value: { state: "DELETED", effect: "APPLIED" },
});
expect(
uploadedBodies
.slice()
.sort((left, right) => left.partNumber - right.partNumber)
.map(({ partNumber, bytes }) => ({ partNumber, bytes })),
).toEqual([
{ partNumber: 1, bytes: [1, 2] },
{ partNumber: 2, bytes: [3, 4] },
{ partNumber: 3, bytes: [5] },
]);
expect(completedParts.map((part) => part.partNumber)).toEqual([
1, 2, 3,
]);
expect(completedParts.map((part) => part.receiptToken)).toEqual([
"etag-part-1",
"etag-part-2",
"etag-part-3",
]);
expect(controlRequests.map((request) => request.path)).toEqual([
"/uploads/create",
"/uploads/status",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/status",
"/uploads/complete",
]);
expect(JSON.stringify(controlRequests)).not.toContain("?sig=");
expect(JSON.stringify(controlRequests)).not.toContain(OBJECT_ORIGIN);
});