refactor: 프론트 템플릿 리펙토링
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
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);
|
||||
}
|
||||
});});
|
||||
Reference in New Issue
Block a user