421 lines
13 KiB
TypeScript
421 lines
13 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
BrowserFilePolicyRegistry,
|
|
browserFilePolicyReference,
|
|
} from "../../src/adapters/browser-files/browser-file-policy-registry.ts";
|
|
import {
|
|
createDownloadDeliveryAdapter,
|
|
type SaveFileHandle,
|
|
} from "../../src/adapters/browser-files/download-delivery-adapter.ts";
|
|
import {
|
|
CHECKSUM_HEADER,
|
|
CONTROL_ENDPOINT,
|
|
DATA_ORIGIN,
|
|
DIGEST_HEADER,
|
|
DOWNLOAD_HREF,
|
|
DOWNLOAD_PATH,
|
|
NOW,
|
|
POLICY_HEADER,
|
|
REQUEST_BINDING_SHA256,
|
|
UPLOAD_SESSION_ID,
|
|
collect,
|
|
createHarness,
|
|
downloadCapabilityPayload,
|
|
downloadResponse,
|
|
jsonResponse,
|
|
responseWithUrl,
|
|
uploadCapabilityPayload,
|
|
} from "./presigned-transfer-fixture.ts";
|
|
|
|
describe("presigned DownloadDelivery integration", () => {
|
|
it("connects an issued capability to DownloadDeliveryPort without caller digest input", async () => {
|
|
const bytes = new TextEncoder().encode("verified");
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(bytes.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
|
|
const policy = browserFilePolicyReference(
|
|
"download",
|
|
"presigned-stream",
|
|
);
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference: policy,
|
|
download: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
mediaType: "application/octet-stream",
|
|
safeExtension: ".bin",
|
|
maxTransferBytes: 64,
|
|
maxBufferedBytes: 8,
|
|
integrity: "REQUIRED",
|
|
},
|
|
},
|
|
],
|
|
hardLimits: {
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 64,
|
|
maxPreviewBytes: 64,
|
|
maxObjectUrlBytes: 64,
|
|
maxTransferBytes: 64,
|
|
},
|
|
});
|
|
const written: number[] = [];
|
|
const handle: SaveFileHandle = {
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({
|
|
write(chunk) {
|
|
written.push(...chunk);
|
|
},
|
|
});
|
|
},
|
|
};
|
|
const downloads = createDownloadDeliveryAdapter({
|
|
host: { handoff() {} },
|
|
policies,
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 64,
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
throw new TypeError("not used");
|
|
},
|
|
},
|
|
openAuthorizedSource:
|
|
executor.downloadSources.open.bind(executor.downloadSources),
|
|
showSaveFilePicker: async () => handle,
|
|
userActivation: { isActive: true },
|
|
now: () => NOW,
|
|
});
|
|
const result = await downloads.deliver({
|
|
policy,
|
|
source: {
|
|
kind: "AUTHORIZED_STREAM_RESOURCE",
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
},
|
|
suggestedFileName: "artifact.bin",
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
kind: "SAVED",
|
|
integrity: "VERIFIED",
|
|
bytesWritten: bytes.byteLength,
|
|
},
|
|
});
|
|
expect(written).toEqual([...bytes]);
|
|
});
|
|
|
|
/**
|
|
* TR-RR-04. A presigned byte source owns a fetch reader and a capability
|
|
* lease, and its port requires `close()`. The delivery consumer never called
|
|
* it, so every outcome — success, validation failure, writer failure and
|
|
* abort — leaked both.
|
|
*/
|
|
it.each([
|
|
{ label: "success", mode: "SUCCESS" as const },
|
|
{ label: "writer failure", mode: "WRITER_FAILURE" as const },
|
|
{ label: "abort", mode: "ABORT" as const },
|
|
])("closes the presigned source exactly once on $label", async ({ mode }) => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
let closes = 0;
|
|
const controller = new AbortController();
|
|
const source = {
|
|
byteLength: bytes.byteLength,
|
|
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
|
|
capability: undefined as never,
|
|
close() {
|
|
closes += 1;
|
|
},
|
|
async *stream() {
|
|
if (mode === "ABORT") controller.abort();
|
|
yield { ok: true as const, value: bytes };
|
|
},
|
|
};
|
|
const capability = Object.freeze({
|
|
capabilityReceipt: "capability-close-1",
|
|
method: "GET" as const,
|
|
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
|
mediaType: "application/octet-stream",
|
|
byteLength: bytes.byteLength,
|
|
maxBytes: bytes.byteLength,
|
|
expectedSha256: "a".repeat(64),
|
|
expiresAtEpochMs: NOW + 60_000,
|
|
});
|
|
source.capability = capability as never;
|
|
|
|
const closePolicy = browserFilePolicyReference(
|
|
"download",
|
|
"presigned-close",
|
|
);
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference: closePolicy,
|
|
download: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
mediaType: "application/octet-stream",
|
|
safeExtension: ".bin",
|
|
maxTransferBytes: 64,
|
|
maxBufferedBytes: 8,
|
|
integrity: "REQUIRED",
|
|
},
|
|
},
|
|
],
|
|
hardLimits: {
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 64,
|
|
maxPreviewBytes: 64,
|
|
maxObjectUrlBytes: 64,
|
|
maxTransferBytes: 64,
|
|
},
|
|
});
|
|
const handle: SaveFileHandle = {
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({
|
|
write() {
|
|
if (mode === "WRITER_FAILURE") {
|
|
throw new TypeError("writer exploded");
|
|
}
|
|
},
|
|
});
|
|
},
|
|
};
|
|
const downloads = createDownloadDeliveryAdapter({
|
|
host: { handoff() {} },
|
|
policies,
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 64,
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
throw new TypeError("not used");
|
|
},
|
|
},
|
|
openAuthorizedSource: async () =>
|
|
({ ok: true, value: source }) as never,
|
|
showSaveFilePicker: async () => handle,
|
|
userActivation: { isActive: true },
|
|
now: () => NOW,
|
|
});
|
|
|
|
const deliveryResult = await downloads.deliver({
|
|
policy: closePolicy,
|
|
source: {
|
|
kind: "AUTHORIZED_STREAM_RESOURCE",
|
|
resourceId: "resource-1",
|
|
capability: capability as never,
|
|
},
|
|
suggestedFileName: "artifact.bin",
|
|
signal: controller.signal,
|
|
onProgress() {},
|
|
});
|
|
|
|
void deliveryResult;
|
|
expect(closes).toBe(1);
|
|
});
|
|
|
|
/**
|
|
* TR-02. A lease that resolved after the abort already ended the delivery
|
|
* never reached the holder, so nothing closed it: the fetch reader and the
|
|
* capability lease outlived the terminal result.
|
|
*/
|
|
it("closes a source lease that arrives after the delivery was aborted", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
let closes = 0;
|
|
const controller = new AbortController();
|
|
let releaseOpen:
|
|
| ((value: { ok: true; value: unknown }) => void)
|
|
| undefined;
|
|
const source = {
|
|
byteLength: bytes.byteLength,
|
|
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
|
|
capability: undefined as never,
|
|
close() {
|
|
closes += 1;
|
|
},
|
|
async *stream() {
|
|
yield { ok: true as const, value: bytes };
|
|
},
|
|
};
|
|
const capability = Object.freeze({
|
|
capabilityReceipt: "capability-late-1",
|
|
method: "GET" as const,
|
|
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
|
mediaType: "application/octet-stream",
|
|
byteLength: bytes.byteLength,
|
|
maxBytes: bytes.byteLength,
|
|
expectedSha256: "a".repeat(64),
|
|
expiresAtEpochMs: NOW + 60_000,
|
|
});
|
|
source.capability = capability as never;
|
|
|
|
const latePolicy = browserFilePolicyReference("download", "presigned-late");
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference: latePolicy,
|
|
download: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
mediaType: "application/octet-stream",
|
|
safeExtension: ".bin",
|
|
maxTransferBytes: 64,
|
|
maxBufferedBytes: 8,
|
|
integrity: "REQUIRED",
|
|
},
|
|
},
|
|
],
|
|
hardLimits: {
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 64,
|
|
maxPreviewBytes: 64,
|
|
maxObjectUrlBytes: 64,
|
|
maxTransferBytes: 64,
|
|
},
|
|
});
|
|
const downloads = createDownloadDeliveryAdapter({
|
|
host: { handoff() {} },
|
|
policies,
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 64,
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
throw new TypeError("not used");
|
|
},
|
|
},
|
|
// Ignores the signal entirely and resolves only when the test says so.
|
|
openAuthorizedSource: () =>
|
|
new Promise((resolve) => {
|
|
releaseOpen = resolve as never;
|
|
}) as never,
|
|
showSaveFilePicker: async () => ({
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({ write() {} });
|
|
},
|
|
}),
|
|
userActivation: { isActive: true },
|
|
now: () => NOW,
|
|
});
|
|
|
|
const delivering = downloads.deliver({
|
|
policy: latePolicy,
|
|
source: {
|
|
kind: "AUTHORIZED_STREAM_RESOURCE",
|
|
resourceId: "resource-1",
|
|
capability: capability as never,
|
|
},
|
|
suggestedFileName: "artifact.bin",
|
|
signal: controller.signal,
|
|
onProgress() {},
|
|
});
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
controller.abort();
|
|
const delivered = await delivering;
|
|
expect(delivered.ok).toBe(false);
|
|
|
|
// The lease arrives only now, long after the terminal result.
|
|
releaseOpen?.({ ok: true, value: source });
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
expect(closes).toBe(1);
|
|
});
|
|
|
|
it("does not leave a late rejection unhandled after an abort", async () => {
|
|
const unhandled: unknown[] = [];
|
|
const onUnhandled = (reason: unknown) => unhandled.push(reason);
|
|
process.on("unhandledRejection", onUnhandled);
|
|
try {
|
|
const controller = new AbortController();
|
|
let rejectOpen: ((reason: unknown) => void) | undefined;
|
|
const rejectPolicy = browserFilePolicyReference(
|
|
"download",
|
|
"presigned-late-reject",
|
|
);
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference: rejectPolicy,
|
|
download: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
mediaType: "application/octet-stream",
|
|
safeExtension: ".bin",
|
|
maxTransferBytes: 64,
|
|
maxBufferedBytes: 8,
|
|
integrity: "REQUIRED",
|
|
},
|
|
},
|
|
],
|
|
hardLimits: {
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 64,
|
|
maxPreviewBytes: 64,
|
|
maxObjectUrlBytes: 64,
|
|
maxTransferBytes: 64,
|
|
},
|
|
});
|
|
const downloads = createDownloadDeliveryAdapter({
|
|
host: { handoff() {} },
|
|
policies,
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 64,
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
throw new TypeError("not used");
|
|
},
|
|
},
|
|
openAuthorizedSource: () =>
|
|
new Promise((_resolve, reject) => {
|
|
rejectOpen = reject;
|
|
}) as never,
|
|
showSaveFilePicker: async () => ({
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({ write() {} });
|
|
},
|
|
}),
|
|
userActivation: { isActive: true },
|
|
now: () => NOW,
|
|
});
|
|
|
|
const delivering = downloads.deliver({
|
|
policy: rejectPolicy,
|
|
source: {
|
|
kind: "AUTHORIZED_STREAM_RESOURCE",
|
|
resourceId: "resource-1",
|
|
capability: Object.freeze({
|
|
capabilityReceipt: "capability-late-2",
|
|
method: "GET" as const,
|
|
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
|
mediaType: "application/octet-stream",
|
|
byteLength: 3,
|
|
maxBytes: 3,
|
|
expectedSha256: "a".repeat(64),
|
|
expiresAtEpochMs: NOW + 60_000,
|
|
}) as never,
|
|
},
|
|
suggestedFileName: "artifact.bin",
|
|
signal: controller.signal,
|
|
onProgress() {},
|
|
});
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
controller.abort();
|
|
await delivering;
|
|
|
|
rejectOpen?.(new Error("late open failure"));
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
expect(unhandled).toEqual([]);
|
|
} finally {
|
|
process.off("unhandledRejection", onUnhandled);
|
|
}
|
|
});});
|