feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
@@ -0,0 +1,331 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("selects, inspects, streams, and previews a real browser File", async ({
page,
}) => {
await page.goto("/");
await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-files/index.ts";
const {
createBrowserFileRuntime,
browserFilePolicyReference,
} = await import(/* @vite-ignore */ modulePath);
const input = document.createElement("input");
input.id = "capability-file-input";
input.type = "file";
const label = document.createElement("label");
label.htmlFor = input.id;
label.textContent = "PNG file";
const button = document.createElement("button");
button.type = "button";
button.textContent = "Choose test file";
document.body.append(label, input, button);
const filePolicy = browserFilePolicyReference(
"avatar",
"select-inspect-preview-png",
);
const runtime = createBrowserFileRuntime({
input,
policies: [
{
reference: filePolicy,
selection: {
policyId: "avatar-v1",
purpose: "avatar",
classification: "PERSONAL",
multiple: false,
maxCount: 1,
maxFileBytes: 1024,
maxTotalBytes: 1024,
allowEmpty: false,
accept: [
{
mediaType: "image/png",
extensions: [".png"],
},
],
},
inspection: {
policyId: "png-signature-v1",
maxInspectionBytes: 16,
acceptedSignatures: [
{
mediaType: "image/png",
extensions: [".png"],
patterns: [
{
offset: 0,
bytes: [137, 80, 78, 71, 13, 10, 26, 10],
},
],
},
],
},
preview: {
allowedMediaTypes: ["image/png"],
maxPreviewBytes: 1024,
},
},
],
limits: {
hardMaxPreviewBytes: 1024,
hardMaxObjectUrlBytes: 1024,
hardMaxTransferBytes: 4096,
},
download: {
host: Object.freeze({ handoff() {} }),
browserManagedCapabilities: {
resolve() {
return {
ok: false as const,
error: {
code: "POLICY_REJECTED" as const,
operation: "DOWNLOAD" as const,
retryable: false,
recovery: "NONE" as const,
},
};
},
},
},
});
const state = globalThis as typeof globalThis & {
__browserFileResult?: Promise<unknown>;
};
button.addEventListener("click", () => {
state.__browserFileResult = (async () => {
const selected = await runtime.baselinePicker.select({
policy: filePolicy,
});
if (
!selected.ok ||
selected.value.kind !== "SELECTED" ||
selected.value.files.length !== 1
) {
runtime.dispose();
return selected;
}
const candidate = selected.value.files[0]!;
const signal = new AbortController().signal;
const inspected = await runtime.content.inspect({
ref: candidate.ref,
policy: filePolicy,
signal,
});
const range = await runtime.content.readRange({
ref: candidate.ref,
offset: 1,
length: 4,
signal,
});
const opened = await runtime.content.openSource({
ref: candidate.ref,
signal,
});
const streamed: number[] = [];
if (opened.ok) {
for await (const chunk of opened.value.stream(signal)) {
if (chunk.ok) streamed.push(...chunk.value);
}
}
const verificationReceipt = inspected.ok
? inspected.value.verificationReceipt
: null;
if (!verificationReceipt) {
runtime.dispose();
return inspected;
}
const preview = await runtime.previews.create({
ref: candidate.ref,
verificationReceipt,
policy: filePolicy,
maxPreviewBytes: 1024,
signal,
});
let previewBytes: number[] = [];
let previewProtocol: string | null = null;
if (preview.ok) {
previewProtocol = new URL(preview.value.url).protocol;
previewBytes = Array.from(
new Uint8Array(await (await fetch(preview.value.url)).arrayBuffer()),
);
preview.value.release();
}
runtime.baselinePicker.release(candidate.ref);
runtime.dispose();
return {
selected,
inspected,
range: range.ok ? Array.from(range.value) : range,
streamed,
preview: preview.ok ? { ok: true } : preview,
previewBytes,
previewProtocol,
};
})();
});
});
const chooserPromise = page.waitForEvent("filechooser");
await page.getByRole("button", { name: "Choose test file" }).click();
const chooser = await chooserPromise;
const bytes = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3, 4]);
await chooser.setFiles({
name: "sample.png",
mimeType: "image/png",
buffer: bytes,
});
const result = await page.evaluate(async () => {
const state = globalThis as typeof globalThis & {
__browserFileResult?: Promise<unknown>;
};
return await state.__browserFileResult;
});
expect(result).toMatchObject({
selected: {
ok: true,
value: {
kind: "SELECTED",
files: [{ sizeBytes: bytes.byteLength, source: "NATIVE_INPUT" }],
},
},
inspected: {
ok: true,
value: {
byteLength: bytes.byteLength,
detectedMediaType: "image/png",
signature: "MATCHED",
},
},
range: [80, 78, 71, 13],
streamed: Array.from(bytes),
preview: { ok: true },
previewBytes: Array.from(bytes),
previewProtocol: "blob:",
});
});
test("hands a bounded generated Blob to the real browser download path", async ({
page,
}) => {
await page.goto("/");
await page.evaluate(async () => {
const modulePath = "/src/adapters/browser-files/index.ts";
const {
createBrowserFileRuntime,
createAnchorDownloadHost,
browserFilePolicyReference,
} = await import(/* @vite-ignore */ modulePath);
const input = document.createElement("input");
input.type = "file";
input.id = "unused-download-input";
const label = document.createElement("label");
label.htmlFor = input.id;
label.textContent = "Unused file control";
const button = document.createElement("button");
button.type = "button";
button.textContent = "Download generated file";
document.body.append(label, input, button);
const downloadPolicy = browserFilePolicyReference(
"generated-export",
"bounded-object-url-bin",
);
const runtime = createBrowserFileRuntime({
input,
policies: [
{
reference: downloadPolicy,
download: {
strategy: "BOUNDED_OBJECT_URL",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 1024,
maxBufferedBytes: 1024,
integrity: "OPTIONAL",
},
},
],
limits: {
hardMaxPreviewBytes: 1024,
hardMaxObjectUrlBytes: 1024,
hardMaxTransferBytes: 4096,
},
download: {
host: createAnchorDownloadHost(document),
browserManagedCapabilities: {
resolve() {
return {
ok: false as const,
error: {
code: "POLICY_REJECTED" as const,
operation: "DOWNLOAD" as const,
retryable: false,
recovery: "NONE" as const,
},
};
},
},
},
});
const state = globalThis as typeof globalThis & {
__browserDownloadResult?: Promise<unknown>;
};
button.addEventListener("click", () => {
state.__browserDownloadResult = runtime.downloads
.deliver({
policy: downloadPolicy,
source: {
kind: "GENERATED",
bytes: {
byteLength: 5,
async *stream() {
yield {
ok: true as const,
value: new Uint8Array([1, 2]),
};
yield {
ok: true as const,
value: new Uint8Array([3, 4, 5]),
};
},
},
},
suggestedFileName: "report",
maxTransferBytes: 1024,
maxBufferedBytes: 1024,
signal: new AbortController().signal,
onProgress() {},
})
.finally(() => runtime.dispose());
});
});
const downloadPromise = page.waitForEvent("download");
await page
.getByRole("button", { name: "Download generated file" })
.click();
const download = await downloadPromise;
const stream = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
const result = await page.evaluate(async () => {
const state = globalThis as typeof globalThis & {
__browserDownloadResult?: Promise<unknown>;
};
return await state.__browserDownloadResult;
});
expect(download.suggestedFilename()).toBe("report.bin");
expect(Buffer.concat(chunks)).toEqual(Buffer.from([1, 2, 3, 4, 5]));
expect(result).toMatchObject({
ok: true,
value: {
kind: "BROWSER_HANDOFF",
},
});
});
@@ -0,0 +1,290 @@
import {
expect,
test,
type Page,
} from "../support/browser/strict-browser-test.ts";
const TOPIC = "reference-resources";
const TOPIC_VERSION = 1;
const STATE_KEY = "__crossContextInvalidationCapability";
const STORAGE_PULSE_KEY =
"ca-frontend:cache-invalidation:v1:pulse";
type BrowserTransportMode = "BROADCAST" | "STORAGE";
type BrowserDeliverySnapshot = Readonly<{
ordering: string;
sequence: number;
topic: string;
transport: string;
}>;
type BrowserRuntimeSnapshot = Readonly<{
status: string;
deliveries: readonly BrowserDeliverySnapshot[];
pulseRetained: boolean;
}>;
async function installRuntime(
page: Page,
mode: BrowserTransportMode,
cacheEpoch: string,
): Promise<string> {
return await page.evaluate(
async ({
currentCacheEpoch,
currentMode,
stateKey,
topic,
topicVersion,
}) => {
const modulePath =
"/src/adapters/cross-context-invalidation/index.ts";
const { createBrowserCrossContextInvalidationFromHost } =
(await import(
/* @vite-ignore */ modulePath
)) as typeof import("../../src/adapters/cross-context-invalidation/index.ts");
const host: Record<string, unknown> =
currentMode === "BROADCAST"
? (globalThis as unknown as Record<string, unknown>)
: {
crypto: globalThis.crypto,
localStorage: globalThis.localStorage,
addEventListener:
globalThis.addEventListener.bind(globalThis),
removeEventListener:
globalThis.removeEventListener.bind(globalThis),
};
const runtime = createBrowserCrossContextInvalidationFromHost({
host,
cacheEpoch: currentCacheEpoch,
topics: [{ topic, topicVersion }],
});
if (!runtime) {
throw new Error(
"Cross-context invalidation runtime is unavailable.",
);
}
const deliveries: BrowserDeliverySnapshot[] = [];
runtime.subscribe((delivery) => {
deliveries.push({
ordering: delivery.ordering,
sequence: delivery.event.sequence,
topic: delivery.event.topic,
transport: delivery.transport,
});
});
Reflect.set(globalThis, stateKey, { deliveries, runtime });
return runtime.getStatus();
},
{
currentCacheEpoch: cacheEpoch,
currentMode: mode,
stateKey: STATE_KEY,
topic: TOPIC,
topicVersion: TOPIC_VERSION,
},
);
}
async function publish(page: Page) {
return await page.evaluate(
({ stateKey, topic, topicVersion }) => {
const state = Reflect.get(globalThis, stateKey) as
| {
runtime: {
publish(input: {
topic: string;
topicVersion: number;
}): unknown;
};
}
| undefined;
if (!state) throw new Error("Capability runtime is not installed.");
return state.runtime.publish({ topic, topicVersion });
},
{
stateKey: STATE_KEY,
topic: TOPIC,
topicVersion: TOPIC_VERSION,
},
);
}
async function snapshot(page: Page): Promise<BrowserRuntimeSnapshot> {
return await page.evaluate(
({ pulseKey, stateKey }) => {
const state = Reflect.get(globalThis, stateKey) as
| {
deliveries: BrowserDeliverySnapshot[];
runtime: { getStatus(): string };
}
| undefined;
if (!state) throw new Error("Capability runtime is not installed.");
return {
status: state.runtime.getStatus(),
deliveries: structuredClone(state.deliveries),
pulseRetained: localStorage.getItem(pulseKey) !== null,
};
},
{ pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY },
);
}
async function closeRuntime(page: Page): Promise<string | null> {
return await page.evaluate((stateKey) => {
const state = Reflect.get(globalThis, stateKey) as
| { runtime: { close(): void; getStatus(): string } }
| undefined;
if (!state) return null;
state.runtime.close();
return state.runtime.getStatus();
}, STATE_KEY);
}
async function disposeRuntime(page: Page): Promise<void> {
await page.evaluate(
({ pulseKey, stateKey }) => {
const state = Reflect.get(globalThis, stateKey) as
| { runtime: { close(): void } }
| undefined;
state?.runtime.close();
localStorage.removeItem(pulseKey);
Reflect.deleteProperty(globalThis, stateKey);
},
{ pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY },
);
}
test("delivers invalidation through native BroadcastChannel and cleans the receiver", async ({
page,
}) => {
const secondPage = await page.context().newPage();
const cacheEpoch = `browser.primary.${Date.now()}`;
try {
await Promise.all([page.goto("/"), secondPage.goto("/")]);
await page.evaluate(
(pulseKey) => localStorage.removeItem(pulseKey),
STORAGE_PULSE_KEY,
);
const statuses = await Promise.all([
installRuntime(page, "BROADCAST", cacheEpoch),
installRuntime(secondPage, "BROADCAST", cacheEpoch),
]);
expect(statuses).toEqual([
"ACTIVE_BROADCAST",
"ACTIVE_BROADCAST",
]);
expect(await publish(page)).toEqual({
ok: true,
transport: "BROADCAST",
});
await expect
.poll(async () => (await snapshot(secondPage)).deliveries)
.toEqual([
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "BROADCAST",
},
]);
expect((await snapshot(page)).deliveries).toEqual([]);
expect(await closeRuntime(secondPage)).toBe("CLOSED");
expect(await publish(page)).toEqual({
ok: true,
transport: "BROADCAST",
});
await page.waitForTimeout(100);
expect(await snapshot(secondPage)).toEqual({
status: "CLOSED",
deliveries: [
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "BROADCAST",
},
],
pulseRetained: false,
});
} finally {
await Promise.allSettled([
disposeRuntime(page),
disposeRuntime(secondPage),
]);
await secondPage.close();
}
});
test("falls back to native localStorage events and removes the pulse during cleanup", async ({
page,
}) => {
const secondPage = await page.context().newPage();
const cacheEpoch = `browser.fallback.${Date.now()}`;
try {
await Promise.all([page.goto("/"), secondPage.goto("/")]);
await page.evaluate(
(pulseKey) => localStorage.removeItem(pulseKey),
STORAGE_PULSE_KEY,
);
const statuses = await Promise.all([
installRuntime(page, "STORAGE", cacheEpoch),
installRuntime(secondPage, "STORAGE", cacheEpoch),
]);
expect(statuses).toEqual([
"ACTIVE_STORAGE_FALLBACK",
"ACTIVE_STORAGE_FALLBACK",
]);
expect(await publish(page)).toEqual({
ok: true,
transport: "STORAGE",
});
await expect
.poll(async () => (await snapshot(secondPage)).deliveries)
.toEqual([
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "STORAGE",
},
]);
expect(await snapshot(page)).toMatchObject({
deliveries: [],
pulseRetained: false,
});
expect(await snapshot(secondPage)).toMatchObject({
pulseRetained: false,
});
expect(await closeRuntime(secondPage)).toBe("CLOSED");
expect(await publish(page)).toEqual({
ok: true,
transport: "STORAGE",
});
await page.waitForTimeout(100);
expect(await snapshot(secondPage)).toEqual({
status: "CLOSED",
deliveries: [
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "STORAGE",
},
],
pulseRetained: false,
});
} finally {
await Promise.allSettled([
disposeRuntime(page),
disposeRuntime(secondPage),
]);
await secondPage.close();
}
});
@@ -0,0 +1,182 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
const PNG_1X1 = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
);
test("fetches and decodes an allowlisted public immutable CDN image", async ({
page,
}) => {
const cdnRequests: string[] = [];
await page.route("https://images.example.test/**", async (route) => {
cdnRequests.push(route.request().url());
await route.fulfill({
status: 200,
body: PNG_1X1,
headers: {
"access-control-allow-origin": "*",
"cache-control":
"public, max-age=31536000, s-maxage=31536000, immutable",
"content-length": String(PNG_1X1.byteLength),
"content-type": "image/png",
vary: "Accept-Encoding",
},
});
});
await page.goto("/");
const result = await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-transfer/image-cdn/index.ts";
const {
ImageCdnPolicyRegistry,
createBrowserImageProbe,
createImageCdnRuntime,
imageCdnPresetReference,
} = (await import(
/* @vite-ignore */ modulePath
)) as typeof import("../../src/adapters/browser-transfer/image-cdn/index.ts");
const preset = imageCdnPresetReference(
"browser-probe",
"verify-public-cdn-image",
);
const policies = new ImageCdnPolicyRegistry({
applicationOrigin: "https://app.example.test",
origins: [
{
originKey: "browser-images",
origin: "https://images.example.test",
assetPathPrefix: "/v1/assets/",
minimumPublicMaxAgeSeconds: 31_536_000,
},
],
presets: [
{
reference: preset,
bindingId: "browser-probe-v1",
width: 1,
height: 1,
fit: "cover",
dprs: [1],
responsiveWidths: [1],
quality: 90,
formats: ["png"],
sizes: "1px",
loading: "eager",
decoding: "async",
fetchPriority: "high",
referrerPolicy: "no-referrer",
probeMode: "PRIMARY_REQUIRED",
allowUpscale: false,
maxTransformedPixels: 1,
maxDecodedBytes: 4,
maxEncodedBytes: 128,
},
],
hardLimits: {
maxIntrinsicWidth: 1,
maxIntrinsicHeight: 1,
maxSourcePixels: 1,
maxCssDimension: 1,
maxDpr: 1,
maxQuality: 90,
maxCandidateCount: 1,
maxTransformedPixels: 1,
maxDecodedBytes: 4,
maxEncodedBytes: 128,
maxUrlLength: 2_048,
maxCapabilityLifetimeMs: 60_000,
maxClockSkewMs: 1_000,
minCapabilityRemainingMs: 1_000,
maxPresetBindingsPerCapability: 1,
maxConcurrentCapabilityVerifications: 1,
allowedSourceMediaTypes: ["image/png"],
formatQualityCeilings: { png: 90 },
},
capability: {
issuer: "browser-image-bff",
acceptedKeyIds: ["browser-image-key-v1"],
},
});
const runtime = createImageCdnRuntime({
policies,
probe: createBrowserImageProbe(),
});
const accepted = runtime.assets.acceptPublicImmutable({
kind: "ALLOWLISTED_PUBLIC",
originKey: "browser-images",
assetId: "asset_1x1",
revision: "revision_1",
mediaType: "image/png",
contentKind: "RASTER_STATIC",
intrinsicWidth: 1,
intrinsicHeight: 1,
});
if (!accepted.ok) {
runtime.close();
return {
accepted,
resolved: null,
};
}
const resolved = await runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
});
runtime.close();
return {
accepted: { ok: true as const },
resolved,
};
});
expect(result.accepted).toEqual({ ok: true });
expect(result.resolved).toMatchObject({
ok: true,
value: {
width: 1,
height: 1,
fallbackMediaType: "image/png",
srcSet: expect.stringMatching(/ 1w$/u),
sources: [],
loading: "eager",
decoding: "async",
fetchPriority: "high",
referrerPolicy: "no-referrer",
crossOrigin: "anonymous",
delivery: {
class: "PUBLIC_IMMUTABLE",
assetVersion: "revision_1",
browserCache: "PUBLIC_IMMUTABLE",
sharedCache: "PUBLIC_IMMUTABLE",
purge: "REVISION_ROLLOVER",
expiresAtEpochMs: null,
},
decodeBudget: {
maximumCandidatePixels: 1,
maximumDecodedBytes: 4,
maximumEncodedBytes: 128,
},
},
});
if (!result.resolved?.ok) return;
const imageUrl = new URL(result.resolved.value.src);
expect(imageUrl.origin).toBe("https://images.example.test");
expect(imageUrl.pathname).toBe(
"/v1/assets/asset_1x1/revision_1",
);
expect(Object.fromEntries(imageUrl.searchParams)).toEqual({
dpr: "1",
fit: "cover",
format: "png",
height: "1",
preset: "browser-probe-v1",
quality: "90",
width: "1",
});
expect(cdnRequests).toEqual([result.resolved.value.src]);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,221 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("runs put, open, remove and reconcile through a native DedicatedWorker, OPFS and IndexedDB", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const runtimeModulePath =
"/src/adapters/storage/opfs/browser-opfs-runtime.ts";
const policyModulePath =
"/src/adapters/storage/opfs/opfs-policy.ts";
const journalModulePath =
"/src/adapters/storage/opfs/indexeddb-opfs-journal.ts";
const {
createBrowserOpfsRuntime,
inspectBrowserOpfsSupport,
} = (await import(
/* @vite-ignore */ runtimeModulePath
)) as typeof import("../../src/adapters/storage/opfs/browser-opfs-runtime.ts");
const {
DEFAULT_OPFS_RUNTIME_POLICY,
resolveOpfsRuntimePolicy,
} = (await import(
/* @vite-ignore */ policyModulePath
)) as typeof import("../../src/adapters/storage/opfs/opfs-policy.ts");
const { opfsJournalDatabaseName } = (await import(
/* @vite-ignore */ journalModulePath
)) as typeof import("../../src/adapters/storage/opfs/indexeddb-opfs-journal.ts");
const policy = resolveOpfsRuntimePolicy();
const support = inspectBrowserOpfsSupport(policy);
if (!support.ok) {
return {
nativeSupport: false as const,
capabilities: support,
};
}
const suffix = crypto.randomUUID().replaceAll("-", "");
const scope = {
namespace: "durable-objects",
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
} as const;
const storagePolicy = {
owner: "browser-capability",
namespace: scope.namespace,
classification: "PERSONAL",
authority: "LOCAL_FIRST",
accountScope: "OPAQUE_PARTITION",
retention: { kind: "EXPLICIT_DELETE" },
softBudgetBytes: 1024 * 1024,
hardBudgetBytes: 2 * 1024 * 1024,
evictionPriority: "USER_AUTHORED",
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
pressureAction: "RETAIN",
unavailableFallback: "EXPORT_REQUIRED",
} as const;
const runtime = createBrowserOpfsRuntime({
workerUrl: new URL(
"/tests/browser-capabilities/opfs-test.worker.ts",
location.href,
),
workerName: `opfs-capability-${suffix}`,
scope,
storagePolicy,
policy,
});
const bytes = new Uint8Array([3, 1, 4, 1, 5, 9]);
let capabilities: unknown;
let put: unknown;
let opened:
| Awaited<ReturnType<typeof runtime.objects.open>>
| undefined;
let readBytes: number[] = [];
let removed: unknown;
let afterRemove: unknown;
let reconciled: unknown;
try {
capabilities = await runtime.objects.capabilities();
put = await runtime.objects.put({
objectId: "object_browser_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: {
byteLength: bytes.byteLength,
async *stream(signal: AbortSignal) {
if (signal.aborted) {
yield {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "OBJECT_WRITE" as const,
retryable: false,
recovery: "NONE" as const,
},
};
return;
}
yield { ok: true as const, value: Uint8Array.from(bytes) };
},
},
});
opened = await runtime.objects.open({
objectId: "object_browser_12345678",
});
if (
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
) {
for await (const chunk of opened.value.source.stream(
new AbortController().signal,
)) {
if (!chunk.ok) throw new Error(chunk.error.code);
readBytes.push(...chunk.value);
}
}
removed = await runtime.objects.remove({
objectId: "object_browser_12345678",
expectedGeneration: 1,
});
afterRemove = await runtime.objects.open({
objectId: "object_browser_12345678",
});
reconciled = await runtime.maintenance.reconcile({
budgetMs: 5_000,
maxTransactions: 100,
});
} finally {
runtime.close();
}
const deletion = await new Promise<string>((resolve) => {
const request = indexedDB.deleteDatabase(
opfsJournalDatabaseName(scope.authorityToken),
);
request.onsuccess = () => resolve("DELETED");
request.onerror = () =>
resolve(request.error?.name ?? "UNKNOWN_ERROR");
request.onblocked = () => resolve("BLOCKED");
});
let opfsCleanup = "ABSENT";
try {
const originRoot = await navigator.storage.getDirectory();
const runtimeRoot = await originRoot.getDirectoryHandle(
DEFAULT_OPFS_RUNTIME_POLICY.rootDirectoryName,
);
const authorities = await runtimeRoot.getDirectoryHandle(
"authorities",
);
await authorities.removeEntry(scope.authorityToken, {
recursive: true,
});
opfsCleanup = "DELETED";
} catch (error) {
if (!(error instanceof DOMException) || error.name !== "NotFoundError") {
throw error;
}
}
return {
nativeSupport: true as const,
capabilities,
put,
opened:
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
? {
ok: true,
generation: opened.value.descriptor.generation,
}
: opened,
readBytes,
removed,
afterRemove,
reconciled,
deletion,
opfsCleanup,
};
});
if (!result.nativeSupport) {
expect(result.capabilities).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED" },
});
return;
}
expect(result.capabilities).toMatchObject({
ok: true,
value: {
available: true,
dedicatedWorkerRequired: true,
crossContextMutationLockAvailable: true,
},
});
expect(result.put).toMatchObject({
ok: true,
value: { generation: 1, byteLength: 6 },
});
expect(result.opened).toEqual({ ok: true, generation: 1 });
expect(result.readBytes).toEqual([3, 1, 4, 1, 5, 9]);
expect(result.removed).toEqual({ ok: true });
expect(result.afterRemove).toMatchObject({
ok: false,
error: { code: "NOT_FOUND" },
});
expect(result.reconciled).toMatchObject({
ok: true,
value: {
inspectedTransactions: 0,
orphanGcStatus: "COMPLETED",
},
});
expect(result.deletion).toBe("DELETED");
expect(["DELETED", "ABSENT"]).toContain(result.opfsCleanup);
});
@@ -0,0 +1,13 @@
import {
startBrowserOpfsDedicatedWorker,
type OpfsWorkerMessageHost,
} from "../../src/adapters/storage/opfs/opfs-worker-runtime.ts";
void startBrowserOpfsDedicatedWorker(
self as unknown as OpfsWorkerMessageHost,
{
storageManager: navigator.storage,
lockManager: navigator.locks,
crypto,
},
);
@@ -0,0 +1,366 @@
import { createHash } from "node:crypto";
import { expect, test } from "../support/browser/strict-browser-test.ts";
const RESOURCE_ID = "browser-artifact-1";
const OBJECT_ORIGIN = "https://objects.example.test";
const OBJECT_PATH = "/files/browser-artifact-1";
const OBJECT_HREF =
`${OBJECT_ORIGIN}${OBJECT_PATH}?sig=opaque-browser-signature`;
const DOWNLOAD_BYTES = Buffer.from([
0x10, 0x20, 0x30, 0x40, 0x50,
0x60, 0x70, 0x80, 0x90, 0xa0,
]);
const DOWNLOAD_SHA256 = createHash("sha256")
.update(DOWNLOAD_BYTES)
.digest("hex");
test("issues an opaque capability and streams a verified object into the writable save path", async ({
page,
}) => {
const bffRequests: unknown[] = [];
const objectRequests: Readonly<{
url: string;
method: string;
accept: string | undefined;
cookie: string | undefined;
}>[] = [];
await page.route("**/api/download-capability", async (route) => {
bffRequests.push(route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
capabilityReceipt: "browser-download-capability-1",
method: "GET",
binding: {
kind: "DOWNLOAD",
resourceId: RESOURCE_ID,
},
href: OBJECT_HREF,
origin: OBJECT_ORIGIN,
path: OBJECT_PATH,
allowedQueryParameters: ["sig"],
requestHeaders: [
{
name: "accept",
value: "application/octet-stream",
},
],
requiredResponseHeaders: [
{
name: "x-policy-version",
value: "v1",
},
],
digestRequestHeader: null,
digestResponseHeader: "x-content-sha256",
receiptResponseHeader: null,
expectedStatus: 200,
expectedResponseByteLength: null,
mediaType: "application/octet-stream",
byteLength: DOWNLOAD_BYTES.byteLength,
maxBytes: 64,
expectedSha256: DOWNLOAD_SHA256,
expiresAtEpochMs: Date.now() + 5 * 60_000,
singleUse: true,
}),
});
});
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
const headers = route.request().headers();
objectRequests.push({
url: route.request().url(),
method: route.request().method(),
accept: headers.accept,
cookie: headers.cookie,
});
await route.fulfill({
status: 200,
body: DOWNLOAD_BYTES,
headers: {
"access-control-allow-origin": "*",
"access-control-expose-headers":
"content-length, x-content-sha256, x-policy-version",
"content-length": String(DOWNLOAD_BYTES.byteLength),
"content-type": "application/octet-stream",
"x-content-sha256": DOWNLOAD_SHA256,
"x-policy-version": "v1",
},
});
});
await page.goto("/");
const result = await page.evaluate(
async ({ resourceId }) => {
const vaultModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
const providerModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
const executorModulePath =
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
const policyModulePath =
"/src/adapters/browser-files/browser-file-policy-registry.ts";
const deliveryModulePath =
"/src/adapters/browser-files/download-delivery-adapter.ts";
const [
{
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
},
{ createPresignedCapabilityHttpProvider },
{ createPresignedTransferExecutor },
{
BrowserFilePolicyRegistry,
browserFilePolicyReference,
},
{ createDownloadDeliveryAdapter },
] = await Promise.all([
import(
/* @vite-ignore */ vaultModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts")
>,
import(
/* @vite-ignore */ providerModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts")
>,
import(
/* @vite-ignore */ executorModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts")
>,
import(
/* @vite-ignore */ policyModulePath
) as Promise<
typeof import("../../src/adapters/browser-files/browser-file-policy-registry.ts")
>,
import(
/* @vite-ignore */ deliveryModulePath
) as Promise<
typeof import("../../src/adapters/browser-files/download-delivery-adapter.ts")
>,
]);
const vault = createPresignedCapabilityVault({
maxActiveCapabilities: 4,
});
const provider = createPresignedCapabilityHttpProvider({
endpoint: `${location.origin}/api/download-capability`,
vault,
allowedDataOrigins: ["https://objects.example.test"],
allowedDataPathPrefixes: ["/files/"],
allowedQueryParameters: ["sig"],
allowedRequestHeaders: ["accept"],
allowedResponseHeaders: [
"x-content-sha256",
"x-policy-version",
],
hardMaxTransferBytes: 64,
hardMaxUploadResponseBytes: 1_024,
maxCapabilityTtlMs: 10 * 60_000,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
allowInsecureLocalhost: true,
});
const executor = createPresignedTransferExecutor({
vault,
replayGuard: createSingleUsePresignedReplayGuard(),
hardMaxTransferBytes: 64,
hardMaxChunkBytes: 3,
hardMaxUploadResponseBytes: 1_024,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
});
const signal = new AbortController().signal;
const issued = await provider.issueDownload({
resourceId,
signal,
});
if (!issued.ok) {
vault.dispose();
return {
issued,
delivery: null,
capabilityKeys: [],
capabilityBindingKeys: [],
written: [],
writeChunkSizes: [],
writableClosed: false,
writableAborted: false,
progressPhases: [],
};
}
const downloadPolicy = browserFilePolicyReference(
"browser-presigned-download",
"save-verified-artifact",
);
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: downloadPolicy,
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 writeChunkSizes: number[] = [];
let writableClosed = false;
let writableAborted = false;
const downloads = createDownloadDeliveryAdapter({
host: {
handoff() {
throw new TypeError(
"Writable delivery must not use browser handoff.",
);
},
},
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError(
"Browser-managed capability is not used.",
);
},
},
openAuthorizedSource:
executor.downloadSources.open.bind(
executor.downloadSources,
),
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({
write(chunk) {
writeChunkSizes.push(chunk.byteLength);
written.push(...chunk);
},
close() {
writableClosed = true;
},
abort() {
writableAborted = true;
},
});
},
}),
createTransferId: () => "transfer:browser-presigned",
progressMinIntervalMs: 0,
userActivation: { isActive: true },
});
const progressPhases: string[] = [];
const capabilityKeys = Object.keys(issued.value).sort();
const capabilityBindingKeys = Object.keys(
issued.value.binding,
).sort();
const delivery = await downloads.deliver({
policy: downloadPolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId,
capability: issued.value,
},
suggestedFileName: "browser-artifact.bin",
signal,
onProgress(progress) {
progressPhases.push(progress.phase);
},
});
downloads.dispose();
vault.dispose();
return {
issued: {
ok: true as const,
exposedHref: "href" in issued.value,
exposedRequestHeaders: "requestHeaders" in issued.value,
},
delivery,
capabilityKeys,
capabilityBindingKeys,
written,
writeChunkSizes,
writableClosed,
writableAborted,
progressPhases,
};
},
{ resourceId: RESOURCE_ID },
);
expect(bffRequests).toEqual([
{
method: "GET",
binding: {
kind: "DOWNLOAD",
resourceId: RESOURCE_ID,
},
},
]);
expect(objectRequests).toEqual([
{
url: OBJECT_HREF,
method: "GET",
accept: "application/octet-stream",
cookie: undefined,
},
]);
expect(result.issued).toEqual({
ok: true,
exposedHref: false,
exposedRequestHeaders: false,
});
expect(result.capabilityKeys).toEqual([
"binding",
"byteLength",
"capabilityReceipt",
"expectedSha256",
"expiresAtEpochMs",
"maxBytes",
"mediaType",
"method",
]);
expect(result.capabilityBindingKeys).toEqual([
"kind",
"resourceId",
]);
expect(result.delivery).toEqual({
ok: true,
value: {
kind: "SAVED",
transferId: "transfer:browser-presigned",
bytesWritten: DOWNLOAD_BYTES.byteLength,
integrity: "VERIFIED",
},
});
expect(result.written).toEqual([...DOWNLOAD_BYTES]);
expect(result.writeChunkSizes).toEqual([3, 3, 3, 1]);
expect(result.writableClosed).toBe(true);
expect(result.writableAborted).toBe(false);
expect(result.progressPhases).toEqual([
"PREPARING",
"TRANSFERRING",
"TRANSFERRING",
"TRANSFERRING",
"TRANSFERRING",
"VERIFYING",
"FINALIZING",
]);
});
@@ -0,0 +1,312 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("keeps A active across a failed B stage, activates B with A rollback, and cleans only owned stale caches", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const adapterModulePath =
"/src/adapters/cache-storage/public-response-cache-adapter.ts";
const policyModulePath =
"/src/adapters/cache-storage/public-cache-policy.ts";
const {
computePublicCacheManifestDigestHex,
createPublicCacheWebLock,
createPublicResponseCacheAdapter,
} = (await import(
/* @vite-ignore */ adapterModulePath
)) as typeof import("../../src/adapters/cache-storage/public-response-cache-adapter.ts");
const { createDefaultPublicCachePolicy } = (await import(
/* @vite-ignore */ policyModulePath
)) as typeof import("../../src/adapters/cache-storage/public-cache-policy.ts");
const suffix = crypto.randomUUID().replaceAll("-", "");
const releaseAId = `browser-a-${suffix.slice(0, 20)}`;
const releaseBId = `browser-b-${suffix.slice(0, 20)}`;
const unrelatedName = `unrelated-${suffix}`;
const policy = {
...createDefaultPublicCachePolicy(location.origin),
ownedCachePrefix: `cap-${suffix.slice(0, 24)}:`,
mutationLockName: `cap-${suffix}:mutation`,
};
const payloads = {
[`${location.origin}/cache-${suffix}-a.js`]: {
bytes: new TextEncoder().encode("verified-release-a"),
contentType: "application/javascript",
},
[`${location.origin}/cache-${suffix}-b.js`]: {
bytes: new TextEncoder().encode("verified-release-b"),
contentType: "application/javascript",
},
[`${location.origin}/cache-${suffix}-b.css`]: {
bytes: new TextEncoder().encode(".release-b{display:block}"),
contentType: "text/css",
},
} as const;
const [aUrl, bUrl, bSecondUrl] = Object.keys(payloads);
const digestHex = async (bytes: Uint8Array): Promise<string> => {
const digest = await crypto.subtle.digest(
"SHA-256",
Uint8Array.from(bytes),
);
return Array.from(
new Uint8Array(digest),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
};
const asset = async (absoluteUrl: string) => {
const payload = payloads[absoluteUrl as keyof typeof payloads];
return {
absoluteUrl,
expectedByteLength: payload.bytes.byteLength,
expectedContentType: payload.contentType,
integrity: {
algorithm: "SHA-256" as const,
digestHex: await digestHex(payload.bytes),
},
};
};
const assetsA = [await asset(aUrl!)];
const assetsB = [
await asset(bUrl!),
await asset(bSecondUrl!),
];
const manifestA = {
releaseRegistryId: releaseAId,
assets: assetsA,
manifestDigestHex:
await computePublicCacheManifestDigestHex(
crypto,
releaseAId,
assetsA,
policy,
),
};
const manifestB = {
releaseRegistryId: releaseBId,
assets: assetsB,
manifestDigestHex:
await computePublicCacheManifestDigestHex(
crypto,
releaseBId,
assetsB,
policy,
),
};
let corruptSecondBAsset = true;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: caches,
crypto,
mutationLock: createPublicCacheWebLock(
navigator.locks,
policy.mutationLockName,
),
policy,
fetcher: async (request) => {
if (request.signal.aborted) {
throw new DOMException("Aborted.", "AbortError");
}
const payload =
payloads[request.url as keyof typeof payloads];
if (!payload) throw new TypeError("Unexpected cache URL.");
const responseBytes =
corruptSecondBAsset && request.url === bSecondUrl
? new Uint8Array(payload.bytes.byteLength)
: Uint8Array.from(payload.bytes);
return new Response(responseBytes, {
status: 200,
headers: {
"cache-control":
"public, max-age=31536000, immutable",
"content-type": payload.contentType,
"x-account-id": "must-not-persist",
},
});
},
});
const readActive = async (absoluteUrl: string) => {
const matched = await adapter.responses.matchActiveExact({
absoluteUrl,
});
if (!matched.ok || !matched.value) return matched;
const streamed: number[] = [];
for await (const chunk of matched.value.body.stream(
new AbortController().signal,
)) {
if (!chunk.ok) return chunk;
streamed.push(...chunk.value);
}
return {
ok: true as const,
streamed,
headerNames: matched.value.headers.map(([name]) => name),
};
};
try {
const stageA = await adapter.admin.stageRelease(manifestA);
const activateA = await adapter.admin.activateRelease(
manifestA.releaseRegistryId,
manifestA.manifestDigestHex,
);
const matchedABeforeFailedB = await readActive(aUrl!);
const failedStageB =
await adapter.admin.stageRelease(manifestB);
const afterFailedB = await adapter.admin.inspect();
const matchedAAfterFailedB = await readActive(aUrl!);
corruptSecondBAsset = false;
const stageB = await adapter.admin.stageRelease(manifestB);
const activateB = await adapter.admin.activateRelease(
manifestB.releaseRegistryId,
manifestB.manifestDigestHex,
);
const matchedB = await readActive(bUrl!);
const afterActivateB = await adapter.admin.inspect();
const releaseBCacheName = (await caches.keys()).find((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseBId}:`,
),
);
if (!releaseBCacheName) {
throw new Error("B release cache not found.");
}
const releaseBCache = await caches.open(releaseBCacheName);
await releaseBCache.put(
new Request(bUrl!),
new Response(new Uint8Array([0]), {
status: 200,
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
}),
);
const poisoned = await adapter.responses.matchActiveExact({
absoluteUrl: bUrl!,
});
await caches.open(`${policy.ownedCachePrefix}stale`);
await caches.open(unrelatedName);
const cleanup = await adapter.admin.cleanupOwned();
const names = await caches.keys();
return {
stageA,
activateA,
matchedABeforeFailedB,
failedStageB,
afterFailedB,
matchedAAfterFailedB,
stageB,
activateB,
matchedB,
afterActivateB,
poisoned,
cleanup,
unrelatedPreserved: names.includes(unrelatedName),
activeBPreserved: names.some((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseBId}:`,
),
),
rollbackAPreserved: names.some((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseAId}:`,
),
),
remainingOwnedCount: names.filter((name) =>
name.startsWith(policy.ownedCachePrefix),
).length,
expectedABytes: [...payloads[aUrl! as keyof typeof payloads].bytes],
expectedBBytes: [...payloads[bUrl! as keyof typeof payloads].bytes],
};
} finally {
for (const name of await caches.keys()) {
if (
name.startsWith(policy.ownedCachePrefix) ||
name === unrelatedName
) {
await caches.delete(name);
}
}
}
});
expect(result.stageA).toMatchObject({
ok: true,
value: { entryCount: 1 },
});
expect(result.activateA).toMatchObject({ ok: true });
expect(result.matchedABeforeFailedB).toMatchObject({
ok: true,
streamed: result.expectedABytes,
});
expect(result.failedStageB).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(result.afterFailedB).toMatchObject({
ok: true,
value: {
activeReleaseRegistryId: expect.stringMatching(/^browser-a-/u),
releaseCandidates: [
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
verified: true,
}),
],
},
});
expect(result.matchedAAfterFailedB).toMatchObject({
ok: true,
streamed: result.expectedABytes,
});
expect(result.stageB).toMatchObject({
ok: true,
value: { entryCount: 2 },
});
expect(result.activateB).toMatchObject({ ok: true });
expect(result.matchedB).toMatchObject({
ok: true,
streamed: result.expectedBBytes,
});
expect(
(
result.matchedB as Readonly<{
headerNames: readonly string[];
}>
).headerNames,
).not.toContain("x-account-id");
expect(result.afterActivateB).toMatchObject({
ok: true,
value: {
activeReleaseRegistryId: expect.stringMatching(/^browser-b-/u),
releaseCandidates: expect.arrayContaining([
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
verified: true,
}),
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-b-/u),
verified: true,
}),
]),
},
});
expect(result.poisoned).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(result.cleanup).toMatchObject({
ok: true,
value: { deletedOwnedCaches: 1, retainedOwnedCaches: 3 },
});
expect(result.unrelatedPreserved).toBe(true);
expect(result.activeBPreserved).toBe(true);
expect(result.rollbackAPreserved).toBe(true);
expect(result.remainingOwnedCount).toBe(3);
});
@@ -0,0 +1,519 @@
import { createHash } from "node:crypto";
import type { Route } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.ts";
const PAGE_ORIGIN = "http://127.0.0.1:4174";
const API_ORIGIN = "https://upload-api.example.test";
const OBJECT_ORIGIN = "https://upload-objects.example.test";
const SESSION_ID = "session_browser_01";
const RESOURCE_ID = "resource_browser_01";
const UPLOAD_PROTOCOL = "PRESIGNED_MULTIPART_V1";
type JsonRecord = Readonly<Record<string, unknown>>;
function corsHeaders(): Record<string, string> {
return {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "content-type",
"access-control-allow-methods": "POST, OPTIONS",
"access-control-allow-origin": PAGE_ORIGIN,
};
}
async function fulfillJson(
route: Route,
status: number,
value: unknown,
): Promise<void> {
const body = JSON.stringify(value);
await route.fulfill({
status,
body,
headers: {
...corsHeaders(),
"content-length": String(Buffer.byteLength(body)),
"content-type": "application/json; charset=utf-8",
},
});
}
test("uploads three presigned parts through native IndexedDB, Web Locks and fetch", async ({
page,
}) => {
let session: JsonRecord | null = null;
const acceptedParts = new Map<number, JsonRecord>();
const issuedParts = new Map<number, JsonRecord>();
const controlRequests: Readonly<{
path: string;
body: JsonRecord;
}>[] = [];
const uploadedBodies: Readonly<{
partNumber: number;
bytes: number[];
checksum: string | undefined;
}>[] = [];
let completedParts: readonly JsonRecord[] = [];
await page.route(`${API_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({ status: 204, headers: corsHeaders() });
return;
}
const path = new URL(route.request().url()).pathname;
const body = route.request().postDataJSON() as JsonRecord;
controlRequests.push({ path, body });
if (path === "/uploads/create") {
const fingerprint = body.fingerprint as JsonRecord;
session = {
protocol: UPLOAD_PROTOCOL,
sessionId: SESSION_ID,
requestBindingSha256: body.requestBindingSha256,
fingerprint,
partSizeBytes: body.requestedPartSizeBytes,
partCount: fingerprint.partCount,
maxConcurrency: 1,
expiresAtEpochMs: Date.now() + 5 * 60_000,
};
await fulfillJson(route, 201, session);
return;
}
if (path === "/uploads/status") {
if (!session) {
await fulfillJson(route, 404, { state: "NOT_FOUND" });
return;
}
await fulfillJson(route, 200, {
state: "ACTIVE",
session,
acceptedParts: [...acceptedParts.values()].sort(
(left, right) =>
Number(left.partNumber) - Number(right.partNumber),
),
});
return;
}
if (path === "/uploads/complete") {
completedParts = body.orderedParts as readonly JsonRecord[];
await fulfillJson(route, 200, {
state: "QUARANTINED",
protocol: UPLOAD_PROTOCOL,
sessionId: body.sessionId,
requestBindingSha256: body.requestBindingSha256,
fingerprint: body.fingerprint,
resourceId: RESOURCE_ID,
});
return;
}
if (path === "/uploads/abort") {
await fulfillJson(route, 200, { state: "ABORTED" });
return;
}
if (path === "/uploads/part-capability") {
const binding = body.binding as JsonRecord;
const partNumber = Number(binding.partNumber);
const descriptor = {
partNumber,
offset: binding.offset,
byteLength: body.byteLength,
checksumSha256: body.expectedSha256,
};
issuedParts.set(partNumber, descriptor);
const objectPath =
`/uploads/${SESSION_ID}/parts/${String(partNumber)}`;
await fulfillJson(route, 200, {
capabilityReceipt: `browser-part-capability-${String(partNumber)}`,
method: "PUT",
binding,
href:
`${OBJECT_ORIGIN}${objectPath}?sig=opaque-${String(partNumber)}`,
origin: OBJECT_ORIGIN,
path: objectPath,
allowedQueryParameters: ["sig"],
requestHeaders: [
{
name: "content-type",
value: body.mediaType,
},
{
name: "x-checksum-sha256",
value: body.expectedSha256,
},
],
requiredResponseHeaders: [
{
name: "x-policy-version",
value: "v1",
},
],
digestRequestHeader: "x-checksum-sha256",
digestResponseHeader: null,
receiptResponseHeader: "etag",
expectedResponseByteLength: 0,
expectedStatus: 204,
mediaType: body.mediaType,
byteLength: body.byteLength,
maxBytes: body.byteLength,
expectedSha256: body.expectedSha256,
expiresAtEpochMs:
Number(session?.expiresAtEpochMs) - 10_000,
singleUse: true,
});
return;
}
await route.fulfill({ status: 404, headers: corsHeaders() });
});
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
"access-control-allow-headers":
"content-type, x-checksum-sha256",
"access-control-allow-methods": "PUT, OPTIONS",
"access-control-allow-origin": "*",
},
});
return;
}
const match = new URL(route.request().url()).pathname.match(
/\/parts\/([1-9][0-9]*)$/u,
);
const partNumber = Number(match?.[1]);
const body = route.request().postDataBuffer();
if (!body || !Number.isSafeInteger(partNumber)) {
await route.fulfill({ status: 400 });
return;
}
const checksum = route.request().headers()["x-checksum-sha256"];
const actualChecksum = createHash("sha256")
.update(body)
.digest("hex");
const descriptor = issuedParts.get(partNumber);
if (
!descriptor ||
checksum !== actualChecksum ||
descriptor.checksumSha256 !== actualChecksum ||
descriptor.byteLength !== body.byteLength
) {
await route.fulfill({ status: 422 });
return;
}
const receiptToken = `etag-part-${String(partNumber)}`;
acceptedParts.set(partNumber, {
...descriptor,
receiptToken,
});
uploadedBodies.push({
partNumber,
bytes: [...body],
checksum,
});
await route.fulfill({
status: 204,
headers: {
"access-control-allow-origin": "*",
"access-control-expose-headers": "etag, x-policy-version",
etag: `"${receiptToken}"`,
"x-policy-version": "v1",
},
});
});
await page.goto("/");
const result = await page.evaluate(
async ({ apiOrigin, objectOrigin }) => {
const vaultModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
const providerModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
const executorModulePath =
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
const fetchTransportModulePath =
"/src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts";
const controlPlaneModulePath =
"/src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts";
const checkpointModulePath =
"/src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts";
const partExecutorModulePath =
"/src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts";
const uploadRuntimeModulePath =
"/src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts";
const cancellationModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts";
const lockModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts";
const [
{
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
},
{ createPresignedCapabilityHttpProvider },
{ createPresignedTransferExecutor },
{ createResumableUploadFetchJsonTransport },
{ createResumableUploadHttpControlPlane },
{ createIndexedDbResumableUploadCheckpointRuntime },
{ createPresignedUploadPartExecutor },
{ createResumableUploadRuntime },
{ createBrowserUploadCancellationChannel },
{ createResumableUploadWebLock },
] = await Promise.all([
import(/* @vite-ignore */ vaultModulePath),
import(/* @vite-ignore */ providerModulePath),
import(/* @vite-ignore */ executorModulePath),
import(/* @vite-ignore */ fetchTransportModulePath),
import(/* @vite-ignore */ controlPlaneModulePath),
import(/* @vite-ignore */ checkpointModulePath),
import(/* @vite-ignore */ partExecutorModulePath),
import(/* @vite-ignore */ uploadRuntimeModulePath),
import(/* @vite-ignore */ cancellationModulePath),
import(/* @vite-ignore */ lockModulePath),
]);
const suffix = crypto.randomUUID().replaceAll("-", "");
const uploadKey = `upload_browser_${suffix}`;
const checkpointRuntime =
createIndexedDbResumableUploadCheckpointRuntime({
scope: {
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
},
blockedTimeoutMs: 2_000,
});
const vault = createPresignedCapabilityVault({
maxActiveCapabilities: 8,
});
const capabilityProvider =
createPresignedCapabilityHttpProvider({
endpoint: `${apiOrigin}/uploads/part-capability`,
vault,
allowedDataOrigins: [objectOrigin],
allowedDataPathPrefixes: ["/uploads/"],
allowedQueryParameters: ["sig"],
allowedRequestHeaders: [
"content-type",
"x-checksum-sha256",
],
allowedResponseHeaders: ["etag", "x-policy-version"],
hardMaxTransferBytes: 8,
hardMaxUploadResponseBytes: 1_024,
maxCapabilityTtlMs: 10 * 60_000,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
controlPlaneCredentials: "include",
});
const transferExecutor = createPresignedTransferExecutor({
vault,
replayGuard: createSingleUsePresignedReplayGuard(),
hardMaxTransferBytes: 8,
hardMaxChunkBytes: 2,
hardMaxUploadResponseBytes: 1_024,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
});
const transport = createResumableUploadFetchJsonTransport({
endpoints: {
CREATE_SESSION: `${apiOrigin}/uploads/create`,
GET_STATUS: `${apiOrigin}/uploads/status`,
COMPLETE: `${apiOrigin}/uploads/complete`,
ABORT: `${apiOrigin}/uploads/abort`,
},
allowedOrigins: [apiOrigin],
credentials: "include",
timeoutMs: 5_000,
maxRequestBytes: 64 * 1024,
maxResponseBytes: 64 * 1024,
maxRetryAfterMs: 1_000,
});
const controlPlane = createResumableUploadHttpControlPlane({
transport,
partCapabilities: capabilityProvider,
});
const cancellationChannelName =
`browser-upload-cancel-${suffix}`;
const crossContextCancellation =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const cancellationPeer =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const runtime = createResumableUploadRuntime({
controlPlane,
partExecutor: createPresignedUploadPartExecutor(
transferExecutor.uploadParts,
),
checkpoints: checkpointRuntime.store,
mutationLock: createResumableUploadWebLock(
navigator.locks,
"browser-resumable-upload",
),
...(crossContextCancellation
? { crossContextCancellation }
: {}),
crypto,
policy: {
partSizeBytes: 2,
maxFileBytes: 6,
maxPartCount: 3,
maxConcurrency: 2,
maxInFlightBytes: 16,
partBufferCopyFactor: 4,
maxSourceChunkBytes: 2,
maxRetries: 1,
retryBaseDelayMs: 1,
retryMaxDelayMs: 5,
maxRetryAfterMs: 1_000,
capabilityRefreshSkewMs: 1_000,
maxSessionLifetimeMs: 10 * 60_000,
providerAttemptTimeoutMs: 10_000,
},
});
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
const progress: string[] = [];
let upload: unknown;
let checkpoint: unknown;
let deletion: unknown;
let cancellationDelivered = false;
try {
if (crossContextCancellation && cancellationPeer) {
cancellationDelivered = await new Promise<boolean>(
(resolve) => {
const timer = setTimeout(() => resolve(false), 2_000);
const release = cancellationPeer.subscribe((key: string) => {
if (key !== `upload_probe_${suffix}`) return;
clearTimeout(timer);
release();
resolve(true);
});
if (
!crossContextCancellation.publish(
`upload_probe_${suffix}`,
)
) {
clearTimeout(timer);
release();
resolve(false);
}
},
);
}
upload = await runtime.upload({
uploadKey,
purpose: "browser_attachment",
mediaType: "application/octet-stream",
source: {
kind: "RANGE_READER",
reader: {
byteLength: bytes.byteLength,
async readRange(input: {
offset: number;
length: number;
signal: AbortSignal;
}) {
return input.signal.aborted
? {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "FILE_READ" as const,
retryable: false,
recovery: "NONE" as const,
},
}
: {
ok: true as const,
value: bytes.slice(
input.offset,
input.offset + input.length,
),
};
},
},
},
signal: new AbortController().signal,
onProgress(value: { phase: string }) {
progress.push(value.phase);
},
});
checkpoint = await checkpointRuntime.store.read(uploadKey);
} finally {
runtime.close();
cancellationPeer?.close();
deletion =
await checkpointRuntime.admin.deletePartition();
vault.dispose();
}
return {
upload,
checkpoint,
deletion,
progress,
webLocksAvailable: Boolean(navigator.locks),
broadcastCancellationAvailable:
Boolean(crossContextCancellation),
cancellationDelivered,
};
},
{ apiOrigin: API_ORIGIN, objectOrigin: OBJECT_ORIGIN },
);
expect(result.webLocksAvailable).toBe(true);
expect(result.broadcastCancellationAvailable).toBe(true);
expect(result.cancellationDelivered).toBe(true);
expect(result.upload).toMatchObject({
ok: true,
value: {
state: "QUARANTINED",
resourceId: RESOURCE_ID,
byteLength: 5,
replayed: false,
},
});
expect(result.checkpoint).toEqual({ ok: true, value: null });
expect(result.deletion).toEqual({
ok: true,
value: { state: "DELETED" },
});
expect(
uploadedBodies
.slice()
.sort((left, right) => left.partNumber - right.partNumber)
.map(({ partNumber, bytes }) => ({ partNumber, bytes })),
).toEqual([
{ partNumber: 1, bytes: [1, 2] },
{ partNumber: 2, bytes: [3, 4] },
{ partNumber: 3, bytes: [5] },
]);
expect(completedParts.map((part) => part.partNumber)).toEqual([
1, 2, 3,
]);
expect(completedParts.map((part) => part.receiptToken)).toEqual([
"etag-part-1",
"etag-part-2",
"etag-part-3",
]);
expect(controlRequests.map((request) => request.path)).toEqual([
"/uploads/create",
"/uploads/status",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/status",
"/uploads/complete",
]);
expect(JSON.stringify(controlRequests)).not.toContain("?sig=");
expect(JSON.stringify(controlRequests)).not.toContain(OBJECT_ORIGIN);
});
@@ -0,0 +1,29 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("executes the storage durability adapter against the real browser StorageManager", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-file-storage/storage-manager-adapter.ts";
const { createStorageDurabilityAdapter } = await import(
/* @vite-ignore */ modulePath
);
const adapter = createStorageDurabilityAdapter(navigator.storage);
return adapter.inspect();
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(["UNKNOWN", "NORMAL", "PRESSURE", "CRITICAL"]).toContain(
result.value.pressure,
);
expect(
result.value.usageBytes === null || result.value.usageBytes >= 0,
).toBe(true);
expect(
result.value.quotaBytes === null || result.value.quotaBytes >= 0,
).toBe(true);
}
});
-218
View File
@@ -1,218 +0,0 @@
// @vitest-environment jsdom
import {
act,
renderHook,
waitFor,
} from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";
import {
useApplicationMutation,
useApplicationQuery,
} from "../../src/presentation/adapters/query/application-query.js";
import { createFailure } from "../../src/contracts/errors.js";
function queryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 0, gcTime: Infinity },
mutations: { retry: false },
},
});
}
/** @param {QueryClient} client */
function wrapper(client) {
/** @param {{children: React.ReactNode}} props */
return function QueryWrapper({ children }) {
return (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);
};
}
describe("application query inbound bridge", () => {
it("latches a background failure over stale data and clears it on retry success", async () => {
const client = queryClient();
const responses = [
{ ok: /** @type {const} */ (true), value: ["first"] },
{
ok: /** @type {const} */ (false),
error: createFailure("SERVER_FAILURE", "LIST", 0),
},
{ ok: /** @type {const} */ (true), value: ["recovered"] },
];
const execute = vi.fn(async () => responses.shift() ?? responses[0]);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["resource", "list"],
execute,
}),
{ wrapper: wrapper(client) },
);
await waitFor(() => expect(hook.result.current.data).toEqual(["first"]));
await act(() => hook.result.current.retry());
await waitFor(() =>
expect(hook.result.current.state.indicator).toBe("stale-degraded"),
);
expect(hook.result.current.state.base).toBe("success");
expect(hook.result.current.data).toEqual(["first"]);
await act(() => hook.result.current.retry());
await waitFor(() =>
expect(hook.result.current.data).toEqual(["recovered"]),
);
expect(hook.result.current.state.indicator).toBeNull();
expect(execute).toHaveBeenCalledTimes(3);
});
it("projects an initial application failure into terminal state", async () => {
const client = queryClient();
const failure = createFailure("FORBIDDEN", "LIST", 0);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["forbidden"],
execute: async () => ({ ok: false, error: failure }),
}),
{ wrapper: wrapper(client) },
);
await waitFor(() =>
expect(hook.result.current.state.base).toBe("terminal-error"),
);
expect(hook.result.current.state.failure).toBe(failure);
});
it("passes cancellation to the application and does not retain an unmounted error", async () => {
const client = queryClient();
let aborted = false;
const execute = vi.fn(
({ signal }) =>
new Promise((resolve) => {
signal.addEventListener(
"abort",
() => {
aborted = true;
resolve({
ok: false,
error: createFailure("REQUEST_ABORTED", "LIST", 0),
});
},
{ once: true },
);
}),
);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["cancelled"],
execute,
}),
{ wrapper: wrapper(client) },
);
await waitFor(() => expect(execute).toHaveBeenCalledOnce());
hook.unmount();
await waitFor(() => expect(aborted).toBe(true));
expect(client.getQueryState(["cancelled"])?.status).not.toBe("error");
});
});
describe("application mutation inbound bridge", () => {
it("deduplicates submit and commits one optimistic mutation", async () => {
const client = queryClient();
const key = ["resource", "list"];
client.setQueryData(key, ["existing"]);
/** @type {(value: {ok: true, value: string}) => void} */
let complete = () => {};
const execute = vi.fn(
() =>
new Promise((resolve) => {
complete = resolve;
}),
);
const hook = renderHook(
() =>
useApplicationMutation({
execute,
invalidate: [["resource"]],
currentData: client.getQueryData(key),
optimistic: {
queryKey: key,
update: (previous, input) => [
.../** @type {string[]} */ (previous),
input,
],
},
}),
{ wrapper: wrapper(client) },
);
/** @type {ReturnType<typeof hook.result.current.submit> | null} */
let first = null;
/** @type {ReturnType<typeof hook.result.current.submit> | null} */
let duplicate = null;
act(() => {
first = hook.result.current.submit("created");
duplicate = hook.result.current.submit("created");
});
expect(first).toBe(duplicate);
expect(client.getQueryData(key)).toEqual(["existing", "created"]);
await waitFor(() => expect(execute).toHaveBeenCalledOnce());
await waitFor(() =>
expect(hook.result.current.state.indicator).toBe("mutation-pending"),
);
complete({ ok: true, value: "created" });
if (!first) throw new Error("expected pending mutation");
await act(() => first);
expect(client.getQueryState(key)?.isInvalidated).toBe(true);
await waitFor(() =>
expect(hook.result.current.state.indicator).toBeNull(),
);
});
it("rolls optimistic data back and exposes a resolvable conflict", async () => {
const client = queryClient();
const key = ["resource", "list"];
client.setQueryData(key, ["existing"]);
const conflict = createFailure("CONFLICT", "CREATE", 0);
const hook = renderHook(
() =>
useApplicationMutation({
execute: async () => ({ ok: false, error: conflict }),
invalidate: [["resource"]],
currentData: client.getQueryData(key),
optimistic: {
queryKey: key,
update: (previous, input) => [
.../** @type {string[]} */ (previous),
input,
],
},
}),
{ wrapper: wrapper(client) },
);
let outcome;
await act(async () => {
outcome = await hook.result.current.submit("conflicting");
});
expect(outcome).toEqual({ ok: false, error: conflict });
expect(client.getQueryData(key)).toEqual(["existing"]);
expect(hook.result.current.state.indicator).toBe("mutation-conflict");
expect(hook.result.current.state.overlay).toMatchObject({
mutationPending: false,
mutationConflict: true,
});
await act(() => hook.result.current.resolveConflict());
expect(hook.result.current.state.indicator).toBeNull();
expect(client.getQueryState(key)?.isInvalidated).toBe(true);
});
});
+466
View File
@@ -0,0 +1,466 @@
// @vitest-environment jsdom
import {
act,
renderHook,
waitFor,
} from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import {
type ApplicationResult,
useApplicationMutation,
useApplicationQuery,
} from "../../src/presentation/adapters/query/application-query.ts";
import { QueryInvalidationProvider } from "../../src/presentation/adapters/query/query-invalidation-provider.tsx";
import {
defineQueryInvalidationTopic,
type QueryInvalidationCoordinator,
} from "../../src/contracts/query-invalidation.ts";
import { createFailure } from "../../src/contracts/errors.ts";
const RESOURCE_INVALIDATION_TOPIC =
defineQueryInvalidationTopic("resource");
function queryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 0, gcTime: Infinity },
mutations: { retry: false },
},
});
}
function wrapper(client: QueryClient) {
const coordinator: QueryInvalidationCoordinator = {
async invalidate(topics) {
for (const topic of topics) {
await client.invalidateQueries({
queryKey: [topic],
exact: false,
refetchType: "active",
});
}
},
beginMutation() {
return { release: async () => {} };
},
async resetLocal() {
await client.cancelQueries();
client.clear();
},
dispose() {},
};
return function QueryWrapper({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={client}>
<QueryInvalidationProvider coordinator={coordinator}>
{children}
</QueryInvalidationProvider>
</QueryClientProvider>
);
};
}
describe("application query inbound bridge", () => {
it("latches a background failure over stale data and clears it on retry success", async () => {
const client = queryClient();
const responses: ApplicationResult<string[]>[] = [
{ ok: true, value: ["first"] },
{
ok: false,
error: createFailure("SERVER_FAILURE", "LIST", 0),
},
{ ok: true, value: ["recovered"] },
];
const execute = vi.fn(async () => responses.shift() ?? responses[0]);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["resource", "list"],
execute,
}),
{ wrapper: wrapper(client) },
);
await waitFor(() => expect(hook.result.current.data).toEqual(["first"]));
await act(() => hook.result.current.retry());
await waitFor(() =>
expect(hook.result.current.state.indicator).toBe("stale-degraded"),
);
expect(hook.result.current.state.base).toBe("success");
expect(hook.result.current.data).toEqual(["first"]);
await act(() => hook.result.current.retry());
await waitFor(() =>
expect(hook.result.current.data).toEqual(["recovered"]),
);
expect(hook.result.current.state.indicator).toBeNull();
expect(execute).toHaveBeenCalledTimes(3);
});
it("projects an initial application failure into terminal state", async () => {
const client = queryClient();
const failure = createFailure("FORBIDDEN", "LIST", 0);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["forbidden"],
execute: async () => ({ ok: false, error: failure }),
}),
{ wrapper: wrapper(client) },
);
await waitFor(() =>
expect(hook.result.current.state.base).toBe("terminal-error"),
);
expect(hook.result.current.state.failure).toBe(failure);
});
it("normalizes an unexpected execute rejection into a safe terminal failure", async () => {
const client = queryClient();
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["unexpected-rejection"],
execute: async () => {
throw new Error("private upstream detail");
},
}),
{ wrapper: wrapper(client) },
);
await waitFor(() =>
expect(hook.result.current.state.base).toBe("terminal-error"),
);
expect(hook.result.current.state.failure).toMatchObject({
kind: "UNKNOWN_FAILURE",
operationId: "APPLICATION_QUERY",
userMessageKey: "error.unknown_failure",
action: "contact-support",
});
expect(JSON.stringify(hook.result.current.state.failure)).not.toContain(
"private upstream detail",
);
});
it("passes cancellation to the application and does not retain an unmounted error", async () => {
const client = queryClient();
let aborted = false;
const execute = vi.fn(
({ signal }: { signal: AbortSignal }) =>
new Promise<ApplicationResult<unknown>>((resolve) => {
signal.addEventListener(
"abort",
() => {
aborted = true;
resolve({
ok: false,
error: createFailure("REQUEST_ABORTED", "LIST", 0),
});
},
{ once: true },
);
}),
);
const hook = renderHook(
() =>
useApplicationQuery({
queryKey: ["cancelled"],
execute,
}),
{ wrapper: wrapper(client) },
);
await waitFor(() => expect(execute).toHaveBeenCalledOnce());
hook.unmount();
await waitFor(() => expect(aborted).toBe(true));
expect(client.getQueryState(["cancelled"])?.status).not.toBe("error");
});
});
describe("application mutation inbound bridge", () => {
it("deduplicates submit and commits one optimistic mutation", async () => {
const client = queryClient();
const key = ["resource", "list"];
client.setQueryData(key, ["existing"]);
let complete: (value: ApplicationResult<string>) => void = () => {};
const execute = vi.fn(
() =>
new Promise<ApplicationResult<string>>((resolve) => {
complete = resolve;
}),
);
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute,
invalidate: [RESOURCE_INVALIDATION_TOPIC],
currentData: client.getQueryData(key),
optimistic: {
queryKey: key,
update: (previous, input) => [
...(previous as string[]),
input,
],
},
}),
{ wrapper: wrapper(client) },
);
let first: Promise<ApplicationResult<string>> | null = null;
let duplicate: Promise<ApplicationResult<string>> | null = null;
act(() => {
first = hook.result.current.submit("created");
duplicate = hook.result.current.submit("created");
});
expect(first).toBe(duplicate);
await waitFor(() =>
expect(client.getQueryData(key)).toEqual(["existing", "created"]),
);
await waitFor(() => expect(execute).toHaveBeenCalledOnce());
await waitFor(() =>
expect(hook.result.current.state.indicator).toBe("mutation-pending"),
);
complete({ ok: true, value: "created" });
if (!first) throw new Error("expected pending mutation");
await act(() => first);
expect(client.getQueryState(key)?.isInvalidated).toBe(true);
await waitFor(() =>
expect(hook.result.current.state.indicator).toBeNull(),
);
});
it("never joins distinct mutation inputs to the same runtime promise", async () => {
const client = queryClient();
const resolvers = new Map<
string,
(value: ApplicationResult<string>) => void
>();
const execute = vi.fn(
(input: string) =>
new Promise<ApplicationResult<string>>((resolve) => {
resolvers.set(input, resolve);
}),
);
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute,
currentData: true,
}),
{ wrapper: wrapper(client) },
);
let first: Promise<ApplicationResult<string>> | undefined;
let second: Promise<ApplicationResult<string>> | undefined;
act(() => {
first = hook.result.current.submit("first");
second = hook.result.current.submit("second");
});
expect(first).not.toBe(second);
await waitFor(() => expect(execute).toHaveBeenCalledTimes(2));
resolvers.get("first")?.({ ok: true, value: "first" });
resolvers.get("second")?.({ ok: true, value: "second" });
if (!first || !second) throw new Error("expected pending mutations");
await act(async () => {
await Promise.all([first, second]);
});
});
it("cancels an in-flight query before taking the optimistic snapshot", async () => {
const client = queryClient();
const key = ["resource", "ordered-update"];
client.setQueryData(key, ["existing"]);
let finishCancellation: () => void = () => {};
const cancellation = new Promise<void>((resolve) => {
finishCancellation = resolve;
});
const cancelQueries = vi
.spyOn(client, "cancelQueries")
.mockImplementation(async () => cancellation);
const getQueryData = vi.spyOn(client, "getQueryData");
const update = vi.fn((previous, input) => [
...(previous as string[]),
input,
]);
const execute = vi.fn(async () => ({
ok: true as const,
value: "created",
}));
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute,
currentData: ["existing"],
optimistic: { queryKey: key, update },
}),
{ wrapper: wrapper(client) },
);
let pending: Promise<ApplicationResult<string>> | undefined;
act(() => {
pending = hook.result.current.submit("created");
});
expect(cancelQueries).toHaveBeenCalledWith({
queryKey: key,
exact: true,
});
expect(getQueryData).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
expect(execute).not.toHaveBeenCalled();
finishCancellation();
if (!pending) throw new Error("expected pending mutation");
await act(() => pending);
expect(getQueryData).toHaveBeenCalledWith(key);
expect(update).toHaveBeenCalledWith(["existing"], "created");
expect(execute).toHaveBeenCalledOnce();
expect(client.getQueryData(key)).toEqual(["existing", "created"]);
});
it("keeps a committed optimistic update when invalidation fails", async () => {
const client = queryClient();
const key = ["resource", "committed-update"];
client.setQueryData(key, ["existing"]);
vi.spyOn(client, "invalidateQueries").mockRejectedValue(
new Error("cache refresh failed"),
);
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute: async () => ({ ok: true, value: "created" }),
invalidate: [RESOURCE_INVALIDATION_TOPIC],
currentData: ["existing"],
optimistic: {
queryKey: key,
update: (previous, input) => [
...(previous as string[]),
input,
],
},
}),
{ wrapper: wrapper(client) },
);
let outcome;
await act(async () => {
outcome = await hook.result.current.submit("created");
});
expect(outcome).toEqual({ ok: true, value: "created" });
expect(client.getQueryData(key)).toEqual(["existing", "created"]);
});
it("normalizes an optimistic preparation defect without running the command", async () => {
const client = queryClient();
const key = ["resource", "invalid-optimistic-update"];
client.setQueryData(key, ["existing"]);
const execute = vi.fn(async () => ({
ok: true as const,
value: "created",
}));
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute,
currentData: ["existing"],
optimistic: {
queryKey: key,
update: () => {
throw new Error("private optimistic detail");
},
},
}),
{ wrapper: wrapper(client) },
);
let outcome;
await act(async () => {
outcome = await hook.result.current.submit("created");
});
expect(outcome).toMatchObject({
ok: false,
error: {
kind: "UNKNOWN_FAILURE",
operationId: "APPLICATION_MUTATION",
userMessageKey: "error.unknown_failure",
},
});
expect(JSON.stringify(outcome)).not.toContain("private optimistic detail");
expect(execute).not.toHaveBeenCalled();
expect(client.getQueryData(key)).toEqual(["existing"]);
});
it("removes an optimistic cache entry when no prior data existed", async () => {
const client = queryClient();
const key = ["resource", "new-optimistic-entry"];
const failure = createFailure("SERVER_FAILURE", "CREATE", 0);
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute: async () => ({ ok: false, error: failure }),
currentData: true,
optimistic: {
queryKey: key,
update: (_previous, input) => [input],
},
}),
{ wrapper: wrapper(client) },
);
let outcome;
await act(async () => {
outcome = await hook.result.current.submit("temporary");
});
expect(outcome).toEqual({ ok: false, error: failure });
expect(client.getQueryData(key)).toBeUndefined();
expect(client.getQueryState(key)).toBeUndefined();
});
it("rolls optimistic data back and exposes a resolvable conflict", async () => {
const client = queryClient();
const key = ["resource", "list"];
client.setQueryData(key, ["existing"]);
const conflict = createFailure("CONFLICT", "CREATE", 0);
const hook = renderHook(
() =>
useApplicationMutation<string, string>({
execute: async () => ({ ok: false, error: conflict }),
invalidate: [RESOURCE_INVALIDATION_TOPIC],
currentData: client.getQueryData(key),
optimistic: {
queryKey: key,
update: (previous, input) => [
...(previous as string[]),
input,
],
},
}),
{ wrapper: wrapper(client) },
);
let outcome;
await act(async () => {
outcome = await hook.result.current.submit("conflicting");
});
expect(outcome).toEqual({ ok: false, error: conflict });
expect(client.getQueryData(key)).toEqual(["existing"]);
expect(hook.result.current.state.indicator).toBe("mutation-conflict");
expect(hook.result.current.state.overlay).toMatchObject({
mutationPending: false,
mutationConflict: true,
});
await act(() => hook.result.current.resolveConflict());
expect(hook.result.current.state.indicator).toBeNull();
expect(client.getQueryState(key)?.isInvalidated).toBe(true);
});
});
@@ -4,9 +4,9 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { deriveAsyncState } from "../../src/application/view-models/async-state.js";
import { AsyncSurface } from "../../src/presentation/components/async-surface.jsx";
import { createFailure } from "../../src/contracts/errors.js";
import { deriveAsyncState } from "../../src/application/view-models/async-state.ts";
import { AsyncSurface } from "../../src/presentation/components/async-surface.tsx";
import { createFailure } from "../../src/contracts/errors.ts";
describe("async UI state matrix", () => {
it.each([
@@ -106,6 +106,42 @@ describe("async UI state matrix", () => {
expect(screen.getByRole("alert")).not.toHaveTextContent("stack");
});
it("routes terminal actions by failure semantics", async () => {
const user = userEvent.setup();
const retry = vi.fn();
const action = vi.fn();
const forbidden = deriveAsyncState({
failure: createFailure("FORBIDDEN", "LIST", 0),
});
const view = render(
<AsyncSurface
state={forbidden}
onAction={action}
onRetry={retry}
/>,
);
await user.click(
screen.getByRole("button", { name: "안전한 화면으로 이동" }),
);
expect(action).toHaveBeenCalledOnce();
expect(retry).not.toHaveBeenCalled();
const retryable = deriveAsyncState({
failure: createFailure("SERVER_FAILURE", "LIST", 0),
});
view.rerender(
<AsyncSurface
state={retryable}
onAction={action}
onRetry={retry}
/>,
);
await user.click(screen.getByRole("button", { name: "다시 시도" }));
expect(retry).toHaveBeenCalledOnce();
expect(action).toHaveBeenCalledOnce();
});
it("does not retain a terminal error after usable data is restored", () => {
const failed = deriveAsyncState({
failure: createFailure("SERVER_FAILURE", "LIST", 0),
@@ -3,10 +3,10 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SafeText } from "../../src/presentation/security/safe-text.jsx";
import { assertSafeConfigNames } from "../../src/contracts/env.js";
import { defineStorageKey } from "../../src/contracts/storage-keys.js";
import { projectTelemetryEvent } from "../../src/contracts/telemetry.js";
import { SafeText } from "../../src/presentation/security/safe-text.tsx";
import { assertSafeConfigNames } from "../../src/contracts/env.ts";
import { defineStorageKey } from "../../src/contracts/storage-keys.ts";
import { projectTelemetryEvent } from "../../src/contracts/telemetry.ts";
describe("browser security boundary", () => {
it("renders untrusted text without script or inline handler injection", () => {
@@ -31,6 +31,7 @@ describe("browser security boundary", () => {
backend: "sessionStorage",
classification: "sensitive-forbidden",
schemaVersion: 1,
valueCodec: "none",
ttl: "session",
migration: "discard",
quotaFallback: "feature-disable",
@@ -6,8 +6,8 @@ import { describe, expect, it, vi } from "vitest";
import {
ChunkRecoveryBoundary,
isChunkLoadFailure,
} from "../../src/presentation/boundaries/chunk-recovery-boundary.js";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.jsx";
} from "../../src/presentation/boundaries/chunk-recovery-boundary.tsx";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.tsx";
function ChunkDefect(): never {
throw new TypeError("Failed to fetch dynamically imported module");
@@ -27,7 +27,7 @@ import {
Tabs,
ToastProvider,
useToast,
} from "../../src/presentation/design-system/index.js";
} from "../../src/presentation/design-system/index.ts";
describe("design-system platform interactions", () => {
it("keeps decorative icons out of the accessibility tree and names icon actions", () => {
@@ -3,8 +3,8 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Button } from "../../src/presentation/components/ui/button.jsx";
import { Card } from "../../src/presentation/components/ui/card.jsx";
import { Button } from "../../src/presentation/components/ui/button.ts";
import { Card } from "../../src/presentation/components/ui/card.ts";
describe("design-token fixture", () => {
it("uses static semantic primitive classes", () => {
+3 -3
View File
@@ -7,8 +7,8 @@ import { createMemoryRouter, RouterProvider, useNavigate } from "react-router-do
import { z } from "zod";
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../src/contracts/errors.js";
import { Button } from "../../src/presentation/components/ui/button.jsx";
import { createFailure } from "../../src/contracts/errors.ts";
import { Button } from "../../src/presentation/components/ui/button.ts";
import {
DirtyNavigationDialog,
ErrorSummary,
@@ -16,7 +16,7 @@ import {
FormField,
useAppForm,
useDirtyNavigationGuard,
} from "../../src/presentation/forms/index.js";
} from "../../src/presentation/forms/index.ts";
type Values = Readonly<Record<"name" | "note", string>>;
+2 -2
View File
@@ -10,12 +10,12 @@ import {
Drawer,
Pagination,
Tabs,
} from "../../src/presentation/design-system/index.js";
} from "../../src/presentation/design-system/index.ts";
import {
LocaleProvider,
useLocale,
type SupportedLocale,
} from "../../src/presentation/i18n/index.js";
} from "../../src/presentation/i18n/index.ts";
function LocaleHarness() {
const { direction, locale, message, setLocale } = useLocale();
@@ -0,0 +1,52 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { PageHeader } from "../../src/presentation/components/page-header.tsx";
describe("page header focus ownership", () => {
it("takes over focus handed off by the route main region", async () => {
const { rerender } = render(
<>
<main id="main-content" tabIndex={-1} />
<PageHeader title="Loading" />
</>,
);
const main = screen.getByRole("main");
main.focus();
rerender(
<>
<main id="main-content" tabIndex={-1} />
<PageHeader title="Loaded" />
</>,
);
await waitFor(() =>
expect(
screen.getByRole("heading", { level: 1, name: "Loaded" }),
).toHaveFocus(),
);
});
it("does not steal focus from a user-controlled element", () => {
const { rerender } = render(
<>
<button type="button">Menu</button>
<PageHeader title="Loading" />
</>,
);
const menu = screen.getByRole("button", { name: "Menu" });
menu.focus();
rerender(
<>
<button type="button">Menu</button>
<PageHeader title="Loaded" />
</>,
);
expect(menu).toHaveFocus();
});
});
+1 -1
View File
@@ -9,7 +9,7 @@ import {
FormPage,
StandardPage,
StatusPage,
} from "../../src/presentation/templates/index.js";
} from "../../src/presentation/templates/index.ts";
describe("page template slot contracts", () => {
it("renders StandardPage minimum and full landmarks with one h1", () => {
@@ -1,17 +1,17 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../src/contracts/errors.js";
import { deriveAsyncState } from "../../src/application/view-models/async-state.js";
import { AsyncSurface } from "../../src/presentation/components/async-surface.jsx";
import { BootErrorShell } from "../../src/presentation/boundaries/boot-error-shell.jsx";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.jsx";
import { createFailure } from "../../src/contracts/errors.ts";
import { deriveAsyncState } from "../../src/application/view-models/async-state.ts";
import { AsyncSurface } from "../../src/presentation/components/async-surface.tsx";
import { BootErrorShell } from "../../src/presentation/boundaries/boot-error-shell.tsx";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.tsx";
/** @returns {import("react").ReactNode} */
function Defect() {
function Defect(): ReactNode {
throw new Error("raw render stack");
}
@@ -4,10 +4,10 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.js";
import { ApplicationProvider } from "../../src/presentation/providers/application-provider.js";
import { AppRouter } from "../../src/presentation/routes/app-router.jsx";
import { createTestApplication } from "../helpers/create-test-application.js";
import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts";
import { ApplicationProvider } from "../../src/presentation/providers/application-provider.tsx";
import { AppRouter } from "../../src/presentation/routes/app-router.tsx";
import { createTestApplication } from "../helpers/create-test-application.ts";
function renderRouter() {
return render(
@@ -3,8 +3,8 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { createRuntimeComposition } from "../../src/bootstrap/create-runtime-composition.js";
import { RuntimeApplication } from "../../src/bootstrap/runtime-application.jsx";
import { createRuntimeComposition } from "../../src/bootstrap/create-runtime-composition.ts";
import { RuntimeApplication } from "../../src/bootstrap/runtime-application.tsx";
const runtimeConfig = {
APP_ENV: "local",
@@ -5,12 +5,12 @@ import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { Alert } from "../../src/presentation/components/ui/alert.jsx";
import { Badge } from "../../src/presentation/components/ui/badge.jsx";
import { Button } from "../../src/presentation/components/ui/button.jsx";
import { Card } from "../../src/presentation/components/ui/card.jsx";
import { Dialog } from "../../src/presentation/components/ui/dialog.jsx";
import { TextField } from "../../src/presentation/components/ui/text-field.jsx";
import { Alert } from "../../src/presentation/components/ui/alert.ts";
import { Badge } from "../../src/presentation/components/ui/badge.ts";
import { Button } from "../../src/presentation/components/ui/button.ts";
import { Card } from "../../src/presentation/components/ui/card.ts";
import { Dialog } from "../../src/presentation/components/ui/dialog.ts";
import { TextField } from "../../src/presentation/components/ui/text-field.ts";
describe("domain-neutral UI primitives", () => {
it("connects field help and validation errors to the input", () => {
@@ -1,6 +1,6 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "../support/browser/strict-browser-test.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
for (const route of Object.values(ROUTE_REGISTRY).map((definition) => {
if (definition.path === "*") return "/not-found";
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("boots the public app shell", async ({ page }) => {
await page.goto("/");
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("boots and navigates the compact production shell", async ({ page }) => {
await page.goto("/");
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("runs menu typeahead, tabs and duplicate toast interactions", async ({
page,
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({
page,
@@ -1,7 +1,7 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import type { Page } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.ts";
/** @param {import("@playwright/test").Page} page */
async function openReferenceForm(page) {
async function openReferenceForm(page: Page) {
await page.goto("/examples/reference-resources/new");
await page.getByRole("button", { name: "로그인 시작" }).click();
await expect(
@@ -1,6 +1,6 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { successEnvelope } from "../mocks/contracts/envelopes.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
import { successEnvelope } from "../mocks/contracts/envelopes.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
test("opens the protected integration route through the local demo seam", async ({
page,
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
page,
@@ -1,5 +1,5 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("persists an explicit color scheme through the storage contract", async ({
page,
@@ -1,4 +1,4 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("validates and reports the common text-field flow", async ({ page }) => {
await page.goto("/examples/ui");
@@ -3,19 +3,22 @@ import { describe, expect, it } from "vitest";
import {
buildRouteUrl,
parseRouteInput,
} from "../../../src/presentation/routes/route-codecs.js";
} from "../../../src/presentation/routes/route-codecs.ts";
import {
mapReferenceOperation,
toReferenceView,
} from "../../../src/features/reference-feature/contracts/reference-mapper.js";
} from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import {
validateReferencePayload,
validateReferenceRequest,
} from "../../../src/features/reference-feature/contracts/reference-schemas.js";
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
import {
REFERENCE_FEATURE_CONTRACT,
REFERENCE_FEATURE_ID,
referenceQueryKeys,
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import type { ReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import { createTestApplication } from "../../helpers/create-test-application.ts";
describe("reference feature boundary contracts", () => {
it("round-trips one canonical filter through URL and query identity", () => {
@@ -53,11 +56,11 @@ describe("reference feature boundary contracts", () => {
{ id: "unsafe", name: 42 },
]),
).toMatchObject({ success: false });
expect(() =>
expect(
mapReferenceOperation("LIST_REFERENCE_RESOURCES", [
{ id: "unsafe", name: 42 },
]),
).toThrow();
).toMatchObject({ ok: false });
});
it("normalizes request input and maps only owned domain fields", () => {
@@ -71,8 +74,10 @@ describe("reference feature boundary contracts", () => {
name: "Example",
createdAt: "2026-07-26T00:00:00.000Z",
});
if (!("id" in model)) throw new Error("expected one model");
expect(toReferenceView(model)).toEqual({
if (!model.ok || !("id" in model.value)) {
throw new Error("expected one model");
}
expect(toReferenceView(model.value)).toEqual({
resourceId: "reference-1",
title: "Example",
createdAt: "2026-07-26T00:00:00.000Z",
@@ -91,8 +96,45 @@ describe("reference feature boundary contracts", () => {
"CREATE_REFERENCE_RESOURCE",
"GET_REFERENCE_RESOURCE",
]);
expect(
REFERENCE_FEATURE_CONTRACT.apiOperations.GET_REFERENCE_RESOURCE,
).toMatchObject({
pathSchema: "ReferenceResourceParams",
pathParameterNames: ["resourceId"],
providerId: "PRIMARY_API",
authProfileId: "REFERENCE_EXTERNAL_BEARER",
csrfProfileId: "NO_CSRF_BEARER",
});
expect(Object.keys(REFERENCE_FEATURE_CONTRACT.queryRegistry)).toEqual([
"REFERENCE_RESOURCE",
]);
});
it("returns the installed input through the typed application registry", () => {
const featureInput = {
listResources: async () => ({ ok: true as const, value: [] }),
createResource: async ({ name }) => ({
ok: true as const,
value: {
resourceId: "created",
title: name,
createdAt: null,
},
}),
getResource: async (resourceId) => ({
ok: true as const,
value: {
resourceId,
title: "Reference",
createdAt: null,
},
}),
} satisfies ReferenceFeatureInput;
const application = createTestApplication({
featureInputs: { [REFERENCE_FEATURE_ID]: featureInput },
});
expect(application.features.has(REFERENCE_FEATURE_ID)).toBe(true);
expect(application.features.get(REFERENCE_FEATURE_ID)).toBe(featureInput);
});
});
@@ -1,14 +1,17 @@
import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../../src/adapters/http/client.js";
import { createReferenceHttpGateway } from "../../../src/features/reference-feature/adapters/reference-http-gateway.js";
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.js";
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.js";
import { createHttpClient } from "../../../src/adapters/http/client.ts";
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
import {
createReferenceHttpGateway,
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import {
validateReferencePayload,
validateReferenceRequest,
} from "../../../src/features/reference-feature/contracts/reference-schemas.js";
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
describe("reference feature diagnostics correlation", () => {
it("preserves route, operation and request correlation through the vertical path", async () => {
@@ -24,6 +27,7 @@ describe("reference feature diagnostics correlation", () => {
>;
const client = createHttpClient({
baseUrl: "https://api.test",
authSession: createDemoSessionAdapter("authenticated"),
fetcher: async () =>
Response.json({
success: true,
@@ -0,0 +1,106 @@
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../../src/contracts/errors.ts";
import {
createReferenceHttpGateway,
type RawReferenceHttpExecutor,
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
import type { ReferenceResource } from "../../../src/features/reference-feature/domain/reference-resource.ts";
const resources = Object.freeze({
first: Object.freeze({
id: "reference-1",
displayName: "First",
createdAt: null,
}),
created: Object.freeze({
id: "reference-2",
displayName: "Created",
createdAt: "2026-07-26T00:00:00.000Z",
}),
}) satisfies Readonly<Record<string, ReferenceResource>>;
describe("reference HTTP operation gateway", () => {
it("builds the exact registered request for every gateway operation", async () => {
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValueOnce({ ok: true, value: [resources.first] })
.mockResolvedValueOnce({ ok: true, value: resources.created })
.mockResolvedValueOnce({ ok: true, value: resources.first });
const gateway = createReferenceHttpGateway({ execute });
const signal = new AbortController().signal;
await expect(
gateway.list({ cursor: "next", limit: 20, tags: ["active"] }, { signal }),
).resolves.toEqual({ ok: true, value: [resources.first] });
await expect(
gateway.create({ name: "Created", note: "safe note" }),
).resolves.toEqual({ ok: true, value: resources.created });
await expect(
gateway.get("reference-1", { signal }),
).resolves.toEqual({ ok: true, value: resources.first });
expect(execute.mock.calls).toEqual([
[
{
operationId: "LIST_REFERENCE_RESOURCES",
routeId: "REFERENCE_RESOURCE_LIST",
searchParams: {
cursor: "next",
limit: 20,
tags: ["active"],
},
signal,
},
],
[
{
operationId: "CREATE_REFERENCE_RESOURCE",
routeId: "REFERENCE_RESOURCE_LIST",
body: { name: "Created", note: "safe note" },
},
],
[
{
operationId: "GET_REFERENCE_RESOURCE",
routeId: "REFERENCE_RESOURCE_DETAIL",
pathParams: { resourceId: "reference-1" },
signal,
},
],
]);
});
it("preserves a validated raw failure without casting it into success", async () => {
const failure = createFailure(
"SCHEMA_MISMATCH",
"GET_REFERENCE_RESOURCE",
0,
);
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValue({ ok: false, error: failure });
const gateway = createReferenceHttpGateway({ execute });
await expect(gateway.get("invalid")).resolves.toEqual({
ok: false,
error: failure,
});
});
it("fails closed when a raw success does not match its operation result", async () => {
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValue({ ok: true, value: { id: "not-a-list" } });
const gateway = createReferenceHttpGateway({ execute });
await expect(gateway.list({ limit: 20 })).resolves.toMatchObject({
ok: false,
error: {
kind: "MAPPING_CONTRACT_VIOLATION",
code: "BOUND_RESULT_TYPE_MISMATCH",
operationId: "LIST_REFERENCE_RESOURCES",
},
});
});
});
@@ -8,21 +8,27 @@ import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.js";
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
import type { AuthSessionPort } from "../../../src/application/ports/auth-session-port.ts";
import type {
ReferenceFeatureInput,
ReferenceResult,
} from "../../../src/features/reference-feature/application/reference-feature-api.js";
import { REFERENCE_FEATURE_ID } from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
import type { ReferenceResourceView } from "../../../src/features/reference-feature/contracts/reference-mapper.js";
import { createFailure } from "../../../src/contracts/errors.js";
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.js";
import { AppRouter } from "../../../src/presentation/routes/app-router.js";
import { createTestApplication } from "../../helpers/create-test-application.js";
} from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import { REFERENCE_FEATURE_ID } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import type { ReferenceResourceView } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import { createFailure } from "../../../src/contracts/errors.ts";
import type { QueryInvalidationCoordinator } from "../../../src/contracts/query-invalidation.ts";
import { QueryInvalidationProvider } from "../../../src/presentation/adapters/query/query-invalidation-provider.tsx";
import { ServerStateScopeProvider } from "../../../src/presentation/adapters/query/server-state-scope-provider.tsx";
import { createServerStateScopeRuntime } from "../../../src/adapters/query-cache/server-state-scope-runtime.ts";
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx";
import { AppRouter } from "../../../src/presentation/routes/app-router.tsx";
import { createTestApplication } from "../../helpers/create-test-application.ts";
function renderReference(
input: ReferenceFeatureInput,
url = "/examples/reference-resources?limit=5",
session: AuthSessionPort = createDemoSessionAdapter("authenticated"),
) {
window.history.pushState({}, "", url);
const client = new QueryClient({
@@ -31,16 +37,34 @@ function renderReference(
mutations: { retry: false },
},
});
const invalidation: QueryInvalidationCoordinator = Object.freeze({
async invalidate() {},
beginMutation() {
return Object.freeze({
async release() {},
});
},
async resetLocal() {},
dispose() {},
});
const serverStateScope = createServerStateScopeRuntime({
session,
queryInvalidation: invalidation,
});
return render(
<QueryClientProvider client={client}>
<ApplicationProvider
application={createTestApplication({
session: createDemoSessionAdapter("authenticated"),
featureInputs: { [REFERENCE_FEATURE_ID]: input },
})}
>
<AppRouter />
</ApplicationProvider>
<ServerStateScopeProvider runtime={serverStateScope}>
<QueryInvalidationProvider coordinator={invalidation}>
<ApplicationProvider
application={createTestApplication({
session,
featureInputs: { [REFERENCE_FEATURE_ID]: input },
})}
>
<AppRouter />
</ApplicationProvider>
</QueryInvalidationProvider>
</ServerStateScopeProvider>
</QueryClientProvider>,
);
}
@@ -120,6 +144,7 @@ describe("reference feature page states", () => {
});
it("renders backend forbidden even when the client access hint allowed entry", async () => {
const user = userEvent.setup();
renderReference(
inputWith({
listResources: async () => ({
@@ -135,6 +160,71 @@ describe("reference feature page states", () => {
expect(await screen.findByRole("alert")).toHaveTextContent(
"이 작업을 수행할 권한이 없습니다.",
);
await user.click(
screen.getByRole("button", { name: "안전한 화면으로 이동" }),
);
expect(
await screen.findByRole("heading", {
level: 1,
name: "Clean Architecture Frontend",
}),
).toBeVisible();
});
it("starts sign-in with the current route for a backend auth failure", async () => {
const user = userEvent.setup();
const demoSession = createDemoSessionAdapter("authenticated");
const beginSignIn = vi.fn(async () => {});
const session = { ...demoSession, beginSignIn };
renderReference(
inputWith({
listResources: async () => ({
ok: false,
error: createFailure(
"AUTH_REQUIRED",
"LIST_REFERENCE_RESOURCES",
0,
),
}),
}),
"/examples/reference-resources?limit=5",
session,
);
await user.click(await screen.findByRole("button", { name: "로그인" }));
expect(beginSignIn).toHaveBeenCalledWith(
"/examples/reference-resources?limit=5",
);
});
it("falls back to a public route when starting sign-in fails", async () => {
const user = userEvent.setup();
const demoSession = createDemoSessionAdapter("authenticated");
const beginSignIn = vi.fn(async () => {
throw new Error("identity provider unavailable");
});
renderReference(
inputWith({
listResources: async () => ({
ok: false,
error: createFailure(
"AUTH_REQUIRED",
"LIST_REFERENCE_RESOURCES",
0,
),
}),
}),
"/examples/reference-resources?limit=5",
{ ...demoSession, beginSignIn },
);
await user.click(await screen.findByRole("button", { name: "로그인" }));
expect(
await screen.findByRole("heading", {
level: 1,
name: "Clean Architecture Frontend",
}),
).toBeVisible();
});
it("deduplicates create, preserves input and surfaces a conflict", async () => {
@@ -12,11 +12,11 @@ import {
vi,
} from "vitest";
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.js";
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.jsx";
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.js";
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.js";
import { createStrictMockServer } from "../../mocks/server.js";
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.ts";
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.tsx";
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.ts";
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.ts";
import { createStrictMockServer } from "../../mocks/server.ts";
const runtimeConfig = {
APP_ENV: "local",
@@ -1,3 +1,3 @@
import { createRuntimeAdapters } from "../../../../src/bootstrap/runtime-adapters.js";
import { createRuntimeAdapters } from "../../../../src/bootstrap/runtime-adapters.ts";
export const bootstrapFactory = createRuntimeAdapters;
@@ -1,3 +1,3 @@
import { createApplication } from "../../../../src/application/create-application.js";
import { createApplication } from "../../../../src/application/create-application.ts";
export const applicationFactory = createApplication;
@@ -1,4 +1,4 @@
import { createApplication } from "../../../../src/application/create-application.js";
import { createApplication } from "../../../../src/application/create-application.ts";
export const applicationFactory = createApplication;
@@ -0,0 +1,5 @@
import type { Value } from "../domain/value.ts";
export function readValue(): Value {
return { id: "fixture-value" };
}
@@ -0,0 +1,3 @@
export type Value = {
readonly id: string;
};
@@ -0,0 +1,5 @@
import { readValue } from "../application/read-value.ts";
export function ValueView() {
return <p>{readValue().id}</p>;
}
@@ -0,0 +1,3 @@
import { second } from "./second.ts";
export const first = `first:${second}`;
@@ -0,0 +1,3 @@
import { first } from "./first.ts";
export const second = `second:${first}`;
@@ -0,0 +1 @@
export const runtimeName = "forbidden-runtime";
@@ -0,0 +1,3 @@
import { runtimeName } from "../adapters/runtime.ts";
export const selectedRuntime = runtimeName;
@@ -0,0 +1,5 @@
const modulePath = "../domain/value.ts";
export async function loadValue() {
return import(modulePath);
}
@@ -0,0 +1,8 @@
import "/definitely-missing-architecture-fixture.ts";
import "architecture-fixture-package-that-does-not-exist";
import "file:///definitely-missing-architecture-fixture.ts";
import type { Value } from "../domain/missing-value.ts";
export function readValue(value: Value) {
return value;
}
@@ -0,0 +1,5 @@
const modulePath = "../domain/value.ts";
export function requireValue() {
return require(modulePath);
}
@@ -1,3 +1,3 @@
import { createHttpClient } from "../../../../src/adapters/http/client.js";
import { createHttpClient } from "../../../../src/adapters/http/client.ts";
export const leakedHttpFactory = createHttpClient;
@@ -1,3 +0,0 @@
import "../../../../src/adapters/http/client.js";
export const invalidEdge = true;
@@ -0,0 +1,3 @@
import "../../../../src/adapters/http/client.ts";
export const invalidEdge = true;
@@ -1,4 +1,4 @@
import "../../../../src/adapters/http/client.js";
import "../../../../src/adapters/http/client.ts";
export function InvalidPresentationFixture() {
return <p>invalid</p>;
@@ -1,4 +1,4 @@
import type { ApplicationOutputPorts } from "../../../../src/application/ports/out/application-output-ports.js";
import type { ApplicationOutputPorts } from "../../../../src/application/ports/out/application-output-ports.ts";
export function OutputPortLeak(_props: ApplicationOutputPorts) {
return <p>forbidden</p>;
@@ -1,4 +1,4 @@
import type { ApplicationApi } from "../../../../src/application/ports/in/application-api.js";
import type { ApplicationApi } from "../../../../src/application/ports/in/application-api.ts";
export function ForbiddenTemplate(_props: { application: ApplicationApi }) {
return <main>Template must not select an application use case.</main>;
@@ -0,0 +1,6 @@
export function createOwnedBrowserStorageCapabilities() {
return {
local: localStorage,
session: sessionStorage,
};
}
@@ -0,0 +1,6 @@
export function createOwnedCrossContextCapabilities() {
return {
channel: new BroadcastChannel("owned-cross-context-fixture"),
storage: localStorage,
};
}
@@ -0,0 +1,17 @@
import type { BrowserStoragePolicy } from "../../../../src/application/ports/browser-file-storage/shared.ts";
export const POLICY: BrowserStoragePolicy = {
owner: "fixture-owner",
namespace: "fixture",
classification: "INTERNAL",
authority: "RECONSTRUCTABLE",
accountScope: "OPAQUE_PARTITION",
retention: { kind: "TTL", maxAgeMs: 1_000 },
softBudgetBytes: 1_000,
hardBudgetBytes: 2_000,
evictionPriority: "RECONSTRUCTABLE",
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
pressureAction: "RETAIN",
unavailableFallback: "ONLINE_ONLY",
};
@@ -0,0 +1,4 @@
export function bypassThroughAlias() {
const browser = globalThis;
return browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,5 @@
class BrowserRootHolder {
readonly root = globalThis;
}
new BrowserRootHolder().root.indexedDB.open("forbidden");
@@ -0,0 +1,4 @@
export function bypassThroughComputedAlias() {
const browser = globalThis;
return browser["caches"].open("forbidden");
}
@@ -0,0 +1,5 @@
class BrowserRootBox {
constructor(readonly root: typeof globalThis) {}
}
new BrowserRootBox(globalThis).root.indexedDB.open("forbidden");
@@ -0,0 +1,3 @@
export function bypassThroughDefault(browser = globalThis) {
return browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function constructBlobOutsideOwnedAdapter() {
return new Blob(["forbidden"]);
}
@@ -0,0 +1,3 @@
export function openBroadcastChannelDirectly() {
return new BroadcastChannel("forbidden");
}
@@ -0,0 +1,3 @@
export function openCacheDirectly() {
return caches.open("forbidden");
}
@@ -0,0 +1,3 @@
export function openDatabaseDirectly() {
return indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function writeLocalStorageDirectly() {
localStorage.setItem("forbidden", "value");
}
@@ -0,0 +1,3 @@
export function openOpfsDirectly() {
return navigator.storage.getDirectory();
}
@@ -0,0 +1,3 @@
export function writeSessionStorageDirectly() {
sessionStorage.setItem("forbidden", "value");
}
@@ -0,0 +1,5 @@
export function bypassThroughDynamicCapabilityKey() {
const browser = globalThis;
const capability = "indexedDB";
return browser[capability].open("forbidden");
}
@@ -0,0 +1,4 @@
export function bypassThroughObjectContainer() {
const holder = { browser: globalThis };
return holder.browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function bypassThroughGlobalThis() {
return globalThis.indexedDB.open("forbidden");
}
@@ -0,0 +1,7 @@
function identity<Value>(value: Value): Value {
return value;
}
export function bypassThroughIdentityWrapper() {
return identity(globalThis).indexedDB.open("forbidden");
}
@@ -0,0 +1,4 @@
const holder: { root?: typeof globalThis } = {};
holder.root = globalThis;
holder.root.indexedDB.open("forbidden");
@@ -0,0 +1,3 @@
export function allocateObjectUrlDirectly(value: never) {
return URL.createObjectURL(value);
}
@@ -0,0 +1,7 @@
export function bypassThroughPropertyDescriptor() {
const descriptor = Object.getOwnPropertyDescriptor(
globalThis,
"indexedDB",
);
return (descriptor?.value as IDBFactory).open("forbidden");
}
@@ -0,0 +1,3 @@
export function bypassThroughReflection() {
return Reflect.get(globalThis, "indexedDB");
}
+1 -1
View File
@@ -1,3 +1,3 @@
import { Button } from "../../../../src/presentation/design-system/primitives/core.js";
import { Button } from "../../../../src/presentation/design-system/primitives/core.tsx";
export const DeepImport = Button;
@@ -1,3 +1,3 @@
import { OPTIONAL_RECIPE_RUNTIME_SENTINEL } from "../../../../../../recipes/frontend-capabilities/index.js";
import { OPTIONAL_RECIPE_RUNTIME_SENTINEL } from "../../../../../../recipes/frontend-capabilities/index.ts";
export const recipeInProduction = OPTIONAL_RECIPE_RUNTIME_SENTINEL;
@@ -0,0 +1,3 @@
import { createIndexedDbRepository } from "../../../adapters/storage/indexeddb/indexeddb-repository.ts";
export const composedWithoutProjectSelection = createIndexedDbRepository;
@@ -0,0 +1,3 @@
export function createOwnedWebSocket(endpoint: string): WebSocket {
return new WebSocket(endpoint, "realtime.v1");
}
@@ -0,0 +1,5 @@
export async function readOwnedPushSubscription(
registration: ServiceWorkerRegistration,
): Promise<PushSubscription | null> {
return registration.pushManager.getSubscription();
}
@@ -0,0 +1,3 @@
import { createWebSocketConnection } from "../../../adapters/realtime/websocket/websocket-connection.ts";
export const incorrectlyComposed = createWebSocketConnection;
@@ -0,0 +1,4 @@
export function createRouteOwnedSocket(endpoint: string): WebSocket {
setInterval(() => undefined, 1_000);
return new WebSocket(endpoint);
}
+2 -2
View File
@@ -4,7 +4,7 @@
"registries": [
{
"registryId": "FIXTURE-SOURCE",
"path": "tests/fixtures/registry/forbidden/invalid-registry.js",
"path": "tests/fixtures/registry/forbidden/invalid-registry.ts",
"exportName": "INVALID_REGISTRY",
"owner": "fixture",
"requiredFields": ["id", "target"],
@@ -19,7 +19,7 @@
},
{
"registryId": "FIXTURE-TARGET",
"path": "tests/fixtures/registry/forbidden/invalid-registry.js",
"path": "tests/fixtures/registry/forbidden/invalid-registry.ts",
"exportName": "TARGET_REGISTRY",
"owner": "fixture",
"requiredFields": ["id"],
+2 -2
View File
@@ -4,7 +4,7 @@
"registries": [
{
"registryId": "ROUTES",
"path": "tests/fixtures/registry/routes/invalid-routes.js",
"path": "tests/fixtures/registry/routes/invalid-routes.ts",
"exportName": "INVALID_ROUTES",
"owner": "fixture",
"requiredFields": [
@@ -34,7 +34,7 @@
},
{
"registryId": "RUNTIME",
"path": "tests/fixtures/registry/routes/invalid-routes.js",
"path": "tests/fixtures/registry/routes/invalid-routes.ts",
"exportName": "INVALID_RUNTIME",
"owner": "fixture",
"requiredFields": ["routeId", "moduleId", "paramsCodec", "searchCodec"],
@@ -0,0 +1,3 @@
export function Fixture({ value }: { value: string }) {
return <span>{value}</span>;
}
-3
View File
@@ -1,3 +0,0 @@
export function Fixture({ value }) {
return <span>{value}</span>;
}
@@ -1,4 +1,4 @@
export function attachScript(source) {
export function attachScript(source: string) {
const script = document.createElement("script");
script.src = source;
document.head.append(script);
-1
View File
@@ -1 +0,0 @@
export const execute = (source) => eval(source);
+1
View File
@@ -0,0 +1 @@
export const execute = (source: string) => eval(source);
@@ -1,3 +1,3 @@
export function RawHtml({ value }) {
export function RawHtml({ value }: { value: string }) {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
}

Some files were not shown because too many files have changed in this diff Show More