Files
clean-architecture-frontend…/tests/unit/presigned-transfer-delivery.test.ts
T

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);
}
});});