Files
clean-architecture-frontend…/tests/unit/browser-file-runtime.test.ts
T

305 lines
7.8 KiB
TypeScript

// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
import type {
FileVerificationReceipt,
LocalFileRef,
} from "../../src/application/ports/browser-file-storage/file.ts";
import {
browserFilePolicyReference,
createBrowserFileRuntime,
type BrowserFilePolicyProfile,
} from "../../src/adapters/browser-files/index.ts";
const filePolicy = browserFilePolicyReference(
"runtime-file",
"select-and-preview",
);
const downloadPolicy = browserFilePolicyReference(
"runtime-download",
"bounded-generated-bin",
);
const policies: readonly BrowserFilePolicyProfile[] = [
{
reference: filePolicy,
selection: {
policyId: "attachment-v1",
purpose: "attachment",
classification: "PERSONAL",
multiple: false,
maxCount: 1,
maxFileBytes: 8,
maxTotalBytes: 8,
allowEmpty: false,
accept: [{ mediaType: "text/plain", extensions: [".txt"] }],
},
inspection: {
policyId: "preview-v1",
maxInspectionBytes: 8,
acceptedSignatures: [
{
mediaType: "image/png",
extensions: [".png"],
patterns: [{ offset: 0, bytes: [0x89] }],
},
],
},
preview: {
allowedMediaTypes: ["image/png"],
maxPreviewBytes: 8,
},
},
{
reference: downloadPolicy,
download: {
strategy: "BOUNDED_OBJECT_URL",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 8,
maxBufferedBytes: 8,
integrity: "OPTIONAL",
},
},
];
const unusedBrowserManagedCapabilities = {
resolve() {
return {
ok: false as const,
error: {
code: "POLICY_REJECTED" as const,
operation: "DOWNLOAD" as const,
retryable: false,
recovery: "NONE" as const,
},
};
},
};
function fileInput(): HTMLInputElement {
const label = document.createElement("label");
label.textContent = "Attachment";
const input = document.createElement("input");
input.type = "file";
label.append(input);
document.body.append(label);
return input;
}
describe("browser file runtime hard limits and disposal", () => {
it("revokes active download leases immediately on runtime dispose", async () => {
const revoked: string[] = [];
let scheduled: (() => void) | undefined;
const runtime = createBrowserFileRuntime({
input: fileInput(),
policies,
limits: {
hardMaxPreviewBytes: 8,
hardMaxObjectUrlBytes: 8,
hardMaxTransferBytes: 16,
},
objectUrlApi: {
createObjectURL: () => "blob:runtime-download",
revokeObjectURL: (url) => revoked.push(url),
},
download: {
host: { handoff: vi.fn() },
browserManagedCapabilities:
unusedBrowserManagedCapabilities,
scheduler: {
setTimeout(callback) {
scheduled = callback;
return 1;
},
},
},
});
const delivery = {
policy: downloadPolicy,
source: {
kind: "GENERATED" as const,
bytes: {
byteLength: 3,
async *stream() {
yield {
ok: true as const,
value: new Uint8Array([1, 2, 3]),
};
},
},
},
suggestedFileName: "artifact",
maxTransferBytes: 8,
maxBufferedBytes: 8,
signal: new AbortController().signal,
onProgress() {},
};
expect(await runtime.downloads.deliver(delivery)).toMatchObject({
ok: true,
value: { kind: "BROWSER_HANDOFF" },
});
expect(revoked).toEqual([]);
runtime.dispose();
expect(revoked).toEqual(["blob:runtime-download"]);
expect(await runtime.downloads.deliver(delivery)).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE" },
});
expect(
await runtime.content.readRange({
ref: "file:disposed" as LocalFileRef,
offset: 0,
length: 0,
signal: new AbortController().signal,
}),
).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE" },
});
expect(
await runtime.previews.create({
ref: "file:disposed" as LocalFileRef,
verificationReceipt:
"verification:disposed" as FileVerificationReceipt,
policy: filePolicy,
maxPreviewBytes: 1,
signal: new AbortController().signal,
}),
).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE" },
});
scheduled?.();
runtime.dispose();
expect(revoked).toEqual(["blob:runtime-download"]);
});
it("aborts a pending native picker and prevents event resurrection", async () => {
const input = fileInput();
Object.defineProperty(input, "showPicker", {
configurable: true,
value: vi.fn(),
});
const runtime = createBrowserFileRuntime({
input,
policies,
limits: {
hardMaxPreviewBytes: 8,
hardMaxObjectUrlBytes: 8,
hardMaxTransferBytes: 16,
},
vault: {
createReference: () => "file:must-not-exist",
},
download: {
host: { handoff() {} },
browserManagedCapabilities:
unusedBrowserManagedCapabilities,
},
});
const pending = runtime.baselinePicker.select({
policy: filePolicy,
});
runtime.dispose();
expect(await pending).toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
Object.defineProperty(input, "files", {
configurable: true,
value: [
new File(["late"], "late.txt", {
type: "text/plain",
lastModified: 1,
}),
],
});
input.dispatchEvent(new Event("change"));
expect(
await runtime.baselinePicker.select({
policy: filePolicy,
}),
).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE" },
});
});
it("aborts a generated download whose producer ignores cancellation", async () => {
let reportStarted: (() => void) | undefined;
const started = new Promise<void>((resolve) => {
reportStarted = resolve;
});
let unblockProducer: (() => void) | undefined;
const producerBlock = new Promise<void>((resolve) => {
unblockProducer = resolve;
});
const runtime = createBrowserFileRuntime({
input: fileInput(),
policies,
limits: {
hardMaxPreviewBytes: 8,
hardMaxObjectUrlBytes: 8,
hardMaxTransferBytes: 16,
},
download: {
host: { handoff() {} },
browserManagedCapabilities:
unusedBrowserManagedCapabilities,
},
});
const pending = runtime.downloads.deliver({
policy: downloadPolicy,
source: {
kind: "GENERATED",
bytes: {
byteLength: 1,
async *stream() {
reportStarted?.();
await producerBlock;
yield {
ok: true as const,
value: new Uint8Array([1]),
};
},
},
},
suggestedFileName: "blocked",
maxTransferBytes: 8,
maxBufferedBytes: 8,
signal: new AbortController().signal,
onProgress() {},
});
await started;
runtime.dispose();
expect(await pending).toMatchObject({
ok: false,
error: { code: "ABORTED", operation: "DOWNLOAD" },
});
unblockProducer?.();
});
it("rejects inconsistent absolute limits during composition", () => {
expect(() =>
createBrowserFileRuntime({
input: fileInput(),
policies,
limits: {
hardMaxPreviewBytes: 8,
hardMaxObjectUrlBytes: 17,
hardMaxTransferBytes: 16,
},
download: {
host: { handoff() {} },
browserManagedCapabilities:
unusedBrowserManagedCapabilities,
},
}),
).toThrowError("Browser file runtime hard limits are invalid.");
});
});