chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,758 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type {
|
||||
FileVerificationReceipt,
|
||||
LocalFileRef,
|
||||
} from "../../src/application/ports/browser-file-storage/file.ts";
|
||||
import { BrowserFileVault } from "../../src/adapters/browser-files/browser-file-vault.ts";
|
||||
import {
|
||||
BrowserTransientPreview,
|
||||
ObjectUrlLeaseRegistry,
|
||||
} from "../../src/adapters/browser-files/object-url-lease.ts";
|
||||
import {
|
||||
sanitizeSuggestedFileName,
|
||||
type RegisteredFileInspectionPolicy,
|
||||
type RegisteredFileSelectionPolicy,
|
||||
} from "../../src/adapters/browser-files/file-policy.ts";
|
||||
import {
|
||||
BrowserFilePolicyRegistry,
|
||||
browserFilePolicyReference,
|
||||
} from "../../src/adapters/browser-files/browser-file-policy-registry.ts";
|
||||
|
||||
const pngSelectionPolicy: RegisteredFileSelectionPolicy = Object.freeze({
|
||||
policyId: "avatar-v1",
|
||||
purpose: "avatar",
|
||||
classification: "PERSONAL",
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxFileBytes: 1_024,
|
||||
maxTotalBytes: 1_024,
|
||||
allowEmpty: false,
|
||||
accept: Object.freeze([
|
||||
Object.freeze({
|
||||
mediaType: "image/png",
|
||||
extensions: Object.freeze([".png"]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const pngInspectionPolicy: RegisteredFileInspectionPolicy = Object.freeze({
|
||||
policyId: "avatar-v1",
|
||||
maxInspectionBytes: 16,
|
||||
acceptedSignatures: Object.freeze([
|
||||
Object.freeze({
|
||||
mediaType: "image/png",
|
||||
extensions: Object.freeze([".png"]),
|
||||
patterns: Object.freeze([
|
||||
Object.freeze({
|
||||
offset: 0,
|
||||
bytes: Object.freeze([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const pngPolicy = browserFilePolicyReference(
|
||||
"avatar",
|
||||
"inspect-and-preview-png",
|
||||
);
|
||||
|
||||
function pngPolicies(
|
||||
maxPreviewBytes = 10,
|
||||
): BrowserFilePolicyRegistry {
|
||||
return new BrowserFilePolicyRegistry({
|
||||
profiles: [
|
||||
{
|
||||
reference: pngPolicy,
|
||||
inspection: pngInspectionPolicy,
|
||||
preview: {
|
||||
allowedMediaTypes: ["image/png"],
|
||||
maxPreviewBytes,
|
||||
},
|
||||
},
|
||||
],
|
||||
hardLimits: {
|
||||
maxInspectionBytes: 64 * 1024,
|
||||
maxRetainedFileBytes: 2_048,
|
||||
maxPreviewBytes,
|
||||
maxObjectUrlBytes: 2_048,
|
||||
maxTransferBytes: 2_048,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("browser file content policy", () => {
|
||||
it("neutralizes path, bidi, device-name and executable filename tricks", () => {
|
||||
expect(
|
||||
sanitizeSuggestedFileName("..\\CON\u202Egpj.exe", {
|
||||
safeExtension: ".pdf",
|
||||
}),
|
||||
).toBe("CONgpj.pdf");
|
||||
expect(
|
||||
sanitizeSuggestedFileName("invoice.exe.pdf", {
|
||||
safeExtension: ".pdf",
|
||||
}),
|
||||
).toBe("invoice_exe.pdf");
|
||||
expect(
|
||||
sanitizeSuggestedFileName("CON.txt", {
|
||||
safeExtension: ".txt",
|
||||
}),
|
||||
).toBe("download.txt");
|
||||
expect(
|
||||
sanitizeSuggestedFileName("report.tar.gz", {
|
||||
safeExtension: ".tar.gz",
|
||||
}),
|
||||
).toBe("report.tar.gz");
|
||||
|
||||
const bounded = sanitizeSuggestedFileName("가".repeat(100), {
|
||||
safeExtension: ".txt",
|
||||
maxUtf8Bytes: 24,
|
||||
});
|
||||
expect(new TextEncoder().encode(bounded).byteLength).toBeLessThanOrEqual(
|
||||
24,
|
||||
);
|
||||
expect(
|
||||
sanitizeSuggestedFileName("😀", {
|
||||
safeExtension: ".bin",
|
||||
maxUtf8Bytes: 5,
|
||||
}),
|
||||
).toBe("d.bin");
|
||||
});
|
||||
|
||||
it("keeps native files behind opaque refs and performs bounded reads", async () => {
|
||||
const policies = pngPolicies();
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () => "file:opaque-1",
|
||||
});
|
||||
const bytes = new Uint8Array([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2,
|
||||
]);
|
||||
const captured = vault.captureFiles(
|
||||
[
|
||||
new File([bytes], "portrait.png", {
|
||||
type: "image/png",
|
||||
lastModified: 100,
|
||||
}),
|
||||
],
|
||||
pngSelectionPolicy,
|
||||
);
|
||||
expect(captured.ok).toBe(true);
|
||||
if (!captured.ok) return;
|
||||
const candidate = captured.value[0];
|
||||
expect(candidate).toBeDefined();
|
||||
if (!candidate) return;
|
||||
expect(candidate.ref).not.toContain(candidate.displayName);
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
expect(
|
||||
await vault.inspect({
|
||||
ref: candidate.ref,
|
||||
policy: pngPolicy,
|
||||
signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
detectedMediaType: "image/png",
|
||||
normalizedExtension: ".png",
|
||||
signature: "MATCHED",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await vault.readRange({
|
||||
ref: candidate.ref,
|
||||
offset: 8,
|
||||
length: 2,
|
||||
signal,
|
||||
}),
|
||||
).toEqual({ ok: true, value: new Uint8Array([1, 2]) });
|
||||
|
||||
const opened = await vault.openSource({
|
||||
ref: candidate.ref,
|
||||
signal,
|
||||
});
|
||||
expect(opened.ok).toBe(true);
|
||||
if (opened.ok) {
|
||||
const streamed: number[] = [];
|
||||
for await (const chunk of opened.value.stream(signal)) {
|
||||
expect(chunk.ok).toBe(true);
|
||||
if (chunk.ok) streamed.push(...chunk.value);
|
||||
}
|
||||
expect(streamed).toEqual([...bytes]);
|
||||
}
|
||||
|
||||
vault.release(candidate.ref);
|
||||
expect(vault.activeVerificationCount).toBe(0);
|
||||
expect(
|
||||
await vault.readRange({
|
||||
ref: candidate.ref,
|
||||
offset: 0,
|
||||
length: 1,
|
||||
signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "NOT_FOUND", recovery: "RESELECT" },
|
||||
});
|
||||
});
|
||||
|
||||
it("snapshots vault requests before asynchronous file resolution", async () => {
|
||||
const references = ["file:a", "file:b"];
|
||||
const policies = pngPolicies(32);
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () =>
|
||||
references.shift() ?? "file:unexpected",
|
||||
createVerificationReceipt: () => "verification:a",
|
||||
});
|
||||
const pngHeader = [
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
];
|
||||
const captured = vault.captureFiles(
|
||||
[
|
||||
new File(
|
||||
[new Uint8Array([...pngHeader, 1, 2])],
|
||||
"a.png",
|
||||
{ type: "image/png", lastModified: 1 },
|
||||
),
|
||||
new File(
|
||||
[new Uint8Array([...pngHeader, 9, 8])],
|
||||
"b.png",
|
||||
{ type: "image/png", lastModified: 2 },
|
||||
),
|
||||
],
|
||||
{
|
||||
...pngSelectionPolicy,
|
||||
multiple: true,
|
||||
maxCount: 2,
|
||||
maxTotalBytes: 2_048,
|
||||
},
|
||||
);
|
||||
expect(captured.ok).toBe(true);
|
||||
if (
|
||||
!captured.ok ||
|
||||
!captured.value[0] ||
|
||||
!captured.value[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const [first, second] = captured.value;
|
||||
const active = new AbortController();
|
||||
const replacement = new AbortController();
|
||||
replacement.abort();
|
||||
|
||||
const readRequest = {
|
||||
ref: first.ref,
|
||||
offset: 8,
|
||||
length: 1,
|
||||
signal: active.signal,
|
||||
};
|
||||
const pendingRead = vault.readRange(readRequest);
|
||||
readRequest.ref = second.ref;
|
||||
readRequest.offset = 9;
|
||||
readRequest.signal = replacement.signal;
|
||||
expect(await pendingRead).toEqual({
|
||||
ok: true,
|
||||
value: new Uint8Array([1]),
|
||||
});
|
||||
|
||||
const inspectionRequest = {
|
||||
ref: first.ref,
|
||||
policy: pngPolicy,
|
||||
signal: active.signal,
|
||||
};
|
||||
const pendingInspection = vault.inspect(inspectionRequest);
|
||||
inspectionRequest.ref = second.ref;
|
||||
inspectionRequest.policy = browserFilePolicyReference(
|
||||
"forged",
|
||||
"forged",
|
||||
);
|
||||
inspectionRequest.signal = replacement.signal;
|
||||
expect(await pendingInspection).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
byteLength: 10,
|
||||
verificationReceipt: "verification:a",
|
||||
},
|
||||
});
|
||||
|
||||
const sourceRequest = {
|
||||
ref: first.ref,
|
||||
signal: active.signal,
|
||||
};
|
||||
const pendingSource = vault.openSource(sourceRequest);
|
||||
sourceRequest.ref = second.ref;
|
||||
sourceRequest.signal = replacement.signal;
|
||||
const opened = await pendingSource;
|
||||
expect(opened.ok).toBe(true);
|
||||
if (!opened.ok) return;
|
||||
const streamed: number[] = [];
|
||||
for await (const chunk of opened.value.stream(active.signal)) {
|
||||
expect(chunk.ok).toBe(true);
|
||||
if (chunk.ok) streamed.push(...chunk.value);
|
||||
}
|
||||
expect(streamed).toEqual([...pngHeader, 1, 2]);
|
||||
});
|
||||
|
||||
it("snapshots file handles and loader methods before awaiting them", async () => {
|
||||
const policies = pngPolicies();
|
||||
let reference = 0;
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () => `file:handle-${reference++}`,
|
||||
});
|
||||
let resolveFirst: ((file: File) => void) | undefined;
|
||||
const firstFile = new File(["a"], "a.txt", {
|
||||
type: "text/plain",
|
||||
lastModified: 1,
|
||||
});
|
||||
const secondFile = new File(["b"], "b.txt", {
|
||||
type: "text/plain",
|
||||
lastModified: 2,
|
||||
});
|
||||
const originalFirst = vi.fn(
|
||||
() =>
|
||||
new Promise<File>((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
);
|
||||
const originalSecond = vi.fn(async () => secondFile);
|
||||
const replaced = vi.fn(async () =>
|
||||
new File(["x"], "x.txt"),
|
||||
);
|
||||
const firstHandle = {
|
||||
kind: "file" as const,
|
||||
name: "a.txt",
|
||||
getFile: originalFirst,
|
||||
};
|
||||
const secondHandle = {
|
||||
kind: "file" as const,
|
||||
name: "b.txt",
|
||||
getFile: originalSecond,
|
||||
};
|
||||
const handles = [firstHandle, secondHandle];
|
||||
const pending = vault.captureHandles(
|
||||
handles,
|
||||
{
|
||||
...pngSelectionPolicy,
|
||||
multiple: true,
|
||||
maxCount: 2,
|
||||
accept: Object.freeze([]),
|
||||
},
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
firstHandle.getFile = replaced;
|
||||
secondHandle.getFile = replaced;
|
||||
handles.splice(1, 1, {
|
||||
kind: "file",
|
||||
name: "x.txt",
|
||||
getFile: replaced,
|
||||
});
|
||||
resolveFirst?.(firstFile);
|
||||
|
||||
const result = await pending;
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: [
|
||||
{ displayName: "a.txt" },
|
||||
{ displayName: "b.txt" },
|
||||
],
|
||||
});
|
||||
expect(originalFirst).toHaveBeenCalledOnce();
|
||||
expect(originalSecond).toHaveBeenCalledOnce();
|
||||
expect(replaced).not.toHaveBeenCalled();
|
||||
if (!result.ok || !result.value[1]) return;
|
||||
expect(
|
||||
await vault.resolveFile(
|
||||
result.value[1].ref,
|
||||
new AbortController().signal,
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: secondFile,
|
||||
});
|
||||
expect(originalSecond).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("marks MIME/extension/signature disagreement and stale handles", async () => {
|
||||
const refs = ["file:mismatch", "file:stale"];
|
||||
const policies = pngPolicies();
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () => refs.shift() ?? "file:unexpected",
|
||||
});
|
||||
const permissive = {
|
||||
...pngSelectionPolicy,
|
||||
accept: Object.freeze([]),
|
||||
};
|
||||
const mismatch = vault.captureFiles(
|
||||
[
|
||||
new File(
|
||||
[
|
||||
new Uint8Array([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]),
|
||||
],
|
||||
"portrait.jpg",
|
||||
{ type: "image/jpeg", lastModified: 100 },
|
||||
),
|
||||
],
|
||||
permissive,
|
||||
);
|
||||
expect(mismatch.ok).toBe(true);
|
||||
if (!mismatch.ok || !mismatch.value[0]) return;
|
||||
expect(
|
||||
await vault.inspect({
|
||||
ref: mismatch.value[0].ref,
|
||||
policy: pngPolicy,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
detectedMediaType: "image/png",
|
||||
signature: "MISMATCHED",
|
||||
verificationReceipt: null,
|
||||
},
|
||||
});
|
||||
|
||||
let current = new File([new Uint8Array([1])], "state.bin", {
|
||||
lastModified: 1,
|
||||
});
|
||||
const handle = {
|
||||
kind: "file" as const,
|
||||
name: "state.bin",
|
||||
getFile: vi.fn(async () => current),
|
||||
};
|
||||
const handleCapture = await vault.captureHandles(
|
||||
[handle],
|
||||
permissive,
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(handleCapture.ok).toBe(true);
|
||||
if (!handleCapture.ok || !handleCapture.value[0]) return;
|
||||
current = new File([new Uint8Array([1, 2])], "state.bin", {
|
||||
lastModified: 2,
|
||||
});
|
||||
expect(
|
||||
await vault.resolveFile(
|
||||
handleCapture.value[0].ref,
|
||||
new AbortController().signal,
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "STALE_RESULT", recovery: "RESELECT" },
|
||||
});
|
||||
});
|
||||
|
||||
it("owns preview object URL leases and rejects active content", async () => {
|
||||
const revoked: string[] = [];
|
||||
const createObjectURL = vi.fn(() => "blob:test-1");
|
||||
const leases = new ObjectUrlLeaseRegistry({
|
||||
createObjectURL,
|
||||
revokeObjectURL: (url) => revoked.push(url),
|
||||
});
|
||||
const policies = pngPolicies();
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () => "file:preview",
|
||||
createVerificationReceipt: () => "verification:preview",
|
||||
});
|
||||
const capture = vault.captureFiles(
|
||||
[
|
||||
new File(
|
||||
[
|
||||
new Uint8Array([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]),
|
||||
],
|
||||
"preview.png",
|
||||
{
|
||||
type: "image/png",
|
||||
lastModified: 1,
|
||||
},
|
||||
),
|
||||
],
|
||||
pngSelectionPolicy,
|
||||
);
|
||||
expect(capture.ok).toBe(true);
|
||||
if (!capture.ok || !capture.value[0]) return;
|
||||
const inspected = await vault.inspect({
|
||||
ref: capture.value[0].ref,
|
||||
policy: pngPolicy,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(inspected.ok).toBe(true);
|
||||
if (!inspected.ok || !inspected.value.verificationReceipt) return;
|
||||
const verificationReceipt =
|
||||
inspected.value.verificationReceipt;
|
||||
const preview = new BrowserTransientPreview({
|
||||
files: vault,
|
||||
policies,
|
||||
leases,
|
||||
hardMaxPreviewBytes: 10,
|
||||
observer: {
|
||||
record() {
|
||||
throw new Error("ignored");
|
||||
},
|
||||
},
|
||||
});
|
||||
const created = await preview.create({
|
||||
ref: capture.value[0].ref,
|
||||
verificationReceipt,
|
||||
policy: pngPolicy,
|
||||
maxPreviewBytes: 10,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(created.ok).toBe(true);
|
||||
if (created.ok) {
|
||||
created.value.release();
|
||||
created.value.release();
|
||||
}
|
||||
expect(revoked).toEqual(["blob:test-1"]);
|
||||
expect(leases.activeLeaseCount).toBe(0);
|
||||
|
||||
expect(
|
||||
await preview.create({
|
||||
ref: capture.value[0].ref,
|
||||
verificationReceipt,
|
||||
policy: pngPolicy,
|
||||
maxPreviewBytes: 11,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "LIMIT_EXCEEDED" },
|
||||
});
|
||||
expect(createObjectURL).toHaveBeenCalledOnce();
|
||||
|
||||
expect(
|
||||
await preview.create({
|
||||
ref: capture.value[0].ref as LocalFileRef,
|
||||
verificationReceipt:
|
||||
"verification:forged" as FileVerificationReceipt,
|
||||
policy: pngPolicy,
|
||||
maxPreviewBytes: 10,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
});
|
||||
|
||||
it("cannot weaken the built-in active-content preview denylist", async () => {
|
||||
const htmlPolicy = browserFilePolicyReference(
|
||||
"active-content",
|
||||
"preview-html",
|
||||
);
|
||||
const policies = new BrowserFilePolicyRegistry({
|
||||
profiles: [
|
||||
{
|
||||
reference: htmlPolicy,
|
||||
inspection: {
|
||||
policyId: "html-signature-v1",
|
||||
maxInspectionBytes: 6,
|
||||
acceptedSignatures: [
|
||||
{
|
||||
mediaType: "text/html",
|
||||
extensions: [".html"],
|
||||
patterns: [
|
||||
{
|
||||
offset: 0,
|
||||
bytes: [60, 104, 116, 109, 108],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
preview: {
|
||||
allowedMediaTypes: ["text/html"],
|
||||
maxPreviewBytes: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
hardLimits: {
|
||||
maxInspectionBytes: 64 * 1024,
|
||||
maxRetainedFileBytes: 1_024,
|
||||
maxPreviewBytes: 10,
|
||||
maxObjectUrlBytes: 1_024,
|
||||
maxTransferBytes: 1_024,
|
||||
},
|
||||
});
|
||||
const vault = new BrowserFileVault({
|
||||
policies,
|
||||
createReference: () => "file:html",
|
||||
createVerificationReceipt: () => "verification:html",
|
||||
});
|
||||
const captured = vault.captureFiles(
|
||||
[
|
||||
new File([new TextEncoder().encode("<html>")], "page.html", {
|
||||
type: "text/html",
|
||||
lastModified: 1,
|
||||
}),
|
||||
],
|
||||
{
|
||||
...pngSelectionPolicy,
|
||||
accept: Object.freeze([]),
|
||||
},
|
||||
);
|
||||
expect(captured.ok).toBe(true);
|
||||
if (!captured.ok || !captured.value[0]) return;
|
||||
const inspected = await vault.inspect({
|
||||
ref: captured.value[0].ref,
|
||||
policy: htmlPolicy,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(inspected.ok).toBe(true);
|
||||
if (!inspected.ok || !inspected.value.verificationReceipt) return;
|
||||
const createObjectURL = vi.fn(() => "blob:must-not-exist");
|
||||
const preview = new BrowserTransientPreview({
|
||||
files: vault,
|
||||
policies,
|
||||
hardMaxPreviewBytes: 10,
|
||||
hardForbiddenMediaTypes: new Set(["image/jpeg"]),
|
||||
leases: new ObjectUrlLeaseRegistry({
|
||||
createObjectURL,
|
||||
revokeObjectURL() {},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
await preview.create({
|
||||
ref: captured.value[0].ref,
|
||||
verificationReceipt: inspected.value.verificationReceipt,
|
||||
policy: htmlPolicy,
|
||||
maxPreviewBytes: 10,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
expect(createObjectURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects count and zero-byte policy violations before retaining refs", () => {
|
||||
const vault = new BrowserFileVault({
|
||||
policies: pngPolicies(),
|
||||
});
|
||||
expect(vault.captureFiles([], pngSelectionPolicy)).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INVALID_INPUT" },
|
||||
});
|
||||
expect(
|
||||
vault.captureFiles(
|
||||
[new File([], "empty.png", { type: "image/png" })],
|
||||
pngSelectionPolicy,
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "LIMIT_EXCEEDED" },
|
||||
});
|
||||
expect(vault.activeReferenceCount).toBe(0);
|
||||
});
|
||||
|
||||
it("caps retained files and aggregate object URL leases", () => {
|
||||
let referenceSequence = 0;
|
||||
const vault = new BrowserFileVault({
|
||||
policies: pngPolicies(),
|
||||
createReference: () => `file:bounded-${referenceSequence++}`,
|
||||
hardMaxActiveReferences: 1,
|
||||
hardMaxRetainedBytes: 3,
|
||||
});
|
||||
const permissive = {
|
||||
...pngSelectionPolicy,
|
||||
accept: Object.freeze([]),
|
||||
};
|
||||
const first = vault.captureFiles(
|
||||
[new File([new Uint8Array(2)], "one.bin")],
|
||||
permissive,
|
||||
);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(vault.retainedByteLength).toBe(2);
|
||||
expect(
|
||||
vault.captureFiles(
|
||||
[new File([new Uint8Array(2)], "two.bin")],
|
||||
permissive,
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "LIMIT_EXCEEDED" },
|
||||
});
|
||||
if (first.ok && first.value[0]) vault.release(first.value[0].ref);
|
||||
expect(vault.retainedByteLength).toBe(0);
|
||||
|
||||
let urlSequence = 0;
|
||||
const leases = new ObjectUrlLeaseRegistry(
|
||||
{
|
||||
createObjectURL: () => `blob:bounded-${urlSequence++}`,
|
||||
revokeObjectURL() {},
|
||||
},
|
||||
{
|
||||
hardMaxActiveLeases: 1,
|
||||
hardMaxSingleLeaseBytes: 6,
|
||||
hardMaxAggregateLeaseBytes: 6,
|
||||
},
|
||||
);
|
||||
const lease = leases.create(new Blob([new Uint8Array(6)]));
|
||||
expect(leases.aggregateLeaseByteLength).toBe(6);
|
||||
expect(() =>
|
||||
leases.create(new Blob([new Uint8Array(1)])),
|
||||
).toThrowError(DOMException);
|
||||
lease.release();
|
||||
expect(leases.aggregateLeaseByteLength).toBe(0);
|
||||
});
|
||||
|
||||
it("converts native stream exceptions to closed chunk failures", async () => {
|
||||
const file = new File([new Uint8Array([1])], "broken.bin", {
|
||||
lastModified: 1,
|
||||
});
|
||||
Object.defineProperty(file, "stream", {
|
||||
value: () =>
|
||||
new ReadableStream<Uint8Array>({
|
||||
pull() {
|
||||
throw new DOMException("native detail", "NotReadableError");
|
||||
},
|
||||
}),
|
||||
});
|
||||
const vault = new BrowserFileVault({
|
||||
policies: pngPolicies(),
|
||||
createReference: () => "file:broken",
|
||||
});
|
||||
const captured = vault.captureFiles(
|
||||
[file],
|
||||
{ ...pngSelectionPolicy, accept: Object.freeze([]) },
|
||||
);
|
||||
expect(captured.ok).toBe(true);
|
||||
if (!captured.ok || !captured.value[0]) return;
|
||||
const opened = await vault.openSource({
|
||||
ref: captured.value[0].ref,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(opened.ok).toBe(true);
|
||||
if (!opened.ok) return;
|
||||
|
||||
const results = [];
|
||||
for await (const result of opened.value.stream(
|
||||
new AbortController().signal,
|
||||
)) {
|
||||
results.push(result);
|
||||
}
|
||||
expect(results).toEqual([
|
||||
{
|
||||
ok: false,
|
||||
error: {
|
||||
code: "NOT_READABLE",
|
||||
operation: "FILE_READ",
|
||||
retryable: true,
|
||||
recovery: "REOPEN",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user