651 lines
20 KiB
TypeScript
651 lines
20 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { PRESIGNED_TRANSFER_PROTOCOL } from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
|
|
import { createPresignedCapabilityVault } from "../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
|
|
import { sha256Hex } from "../../src/adapters/browser-transfer/presigned/incremental-sha256.ts";
|
|
import {
|
|
CHECKSUM_HEADER,
|
|
CONTROL_ENDPOINT,
|
|
DATA_ORIGIN,
|
|
DIGEST_HEADER,
|
|
DOWNLOAD_HREF,
|
|
DOWNLOAD_PATH,
|
|
NOW,
|
|
POLICY_HEADER,
|
|
REQUEST_BINDING_SHA256,
|
|
UPLOAD_SESSION_ID,
|
|
collect,
|
|
createHarness,
|
|
downloadCapabilityPayload,
|
|
downloadResponse,
|
|
jsonResponse,
|
|
responseWithUrl,
|
|
uploadCapabilityPayload,
|
|
} from "./presigned-transfer-fixture.ts";
|
|
|
|
describe("presigned download stream lifecycle", () => {
|
|
describe("TR-01 the stored capability is the one that was validated", () => {
|
|
const baseRegistration = () => ({
|
|
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
|
capabilityReceipt: "capability-snapshot-1",
|
|
method: "GET" as const,
|
|
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
|
href: `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
|
|
origin: DATA_ORIGIN,
|
|
path: DOWNLOAD_PATH,
|
|
allowedQueryParameters: [],
|
|
requestHeaders: [{ name: "x-safe", value: "1" }],
|
|
requiredResponseHeaders: [],
|
|
digestRequestHeader: null,
|
|
digestResponseHeader: null,
|
|
receiptResponseHeader: null,
|
|
expectedStatus: 200,
|
|
expectedResponseByteLength: 3,
|
|
mediaType: "application/octet-stream",
|
|
byteLength: 3,
|
|
maxBytes: 3,
|
|
expectedSha256: "a".repeat(64),
|
|
expiresAtEpochMs: NOW + 60_000,
|
|
});
|
|
|
|
const freshVault = () =>
|
|
createPresignedCapabilityVault({
|
|
now: () => NOW,
|
|
maxActiveCapabilities: 4,
|
|
});
|
|
|
|
it("refuses a header row that answers differently on a second read", () => {
|
|
const vault = freshVault();
|
|
let nameReads = 0;
|
|
const header = new Proxy(
|
|
{ name: "x-safe", value: "1" },
|
|
{
|
|
getOwnPropertyDescriptor(target, key) {
|
|
if (key === "name") {
|
|
nameReads += 1;
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: nameReads > 1 ? "authorization" : "x-safe",
|
|
};
|
|
}
|
|
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
},
|
|
},
|
|
);
|
|
|
|
const registered = vault.register({
|
|
...baseRegistration(),
|
|
requestHeaders: [header],
|
|
} as never);
|
|
|
|
if (registered.ok) {
|
|
// A single read means the value that was checked is the value stored.
|
|
const resolved = vault.resolve(registered.value);
|
|
expect(resolved.ok).toBe(true);
|
|
if (resolved.ok) {
|
|
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
|
|
"x-safe",
|
|
]);
|
|
}
|
|
}
|
|
vault.dispose();
|
|
});
|
|
|
|
const hostileRegistrations: readonly (readonly [string, () => unknown])[] = [
|
|
[
|
|
"an accessor field",
|
|
() =>
|
|
Object.defineProperty(baseRegistration(), "href", {
|
|
enumerable: true,
|
|
get: () => `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
|
|
}),
|
|
],
|
|
[
|
|
"an inherited field",
|
|
() => Object.assign(Object.create({ injected: true }), baseRegistration()),
|
|
],
|
|
[
|
|
"a symbol field",
|
|
() => ({ ...baseRegistration(), [Symbol.for("injected")]: true }),
|
|
],
|
|
[
|
|
"a non-enumerable own field",
|
|
() =>
|
|
Object.defineProperty(baseRegistration(), "injected", {
|
|
enumerable: false,
|
|
value: true,
|
|
}),
|
|
],
|
|
[
|
|
"a throwing ownKeys trap",
|
|
() =>
|
|
new Proxy(baseRegistration(), {
|
|
ownKeys() {
|
|
throw new TypeError("hostile ownKeys trap");
|
|
},
|
|
}),
|
|
],
|
|
[
|
|
"a null header array",
|
|
() => ({ ...baseRegistration(), requestHeaders: null }),
|
|
],
|
|
[
|
|
"a non-iterable header array",
|
|
() => ({ ...baseRegistration(), requestHeaders: { length: 1 } }),
|
|
],
|
|
[
|
|
"a header row with an extra field",
|
|
() => ({
|
|
...baseRegistration(),
|
|
requestHeaders: [{ name: "x-safe", value: "1", injected: true }],
|
|
}),
|
|
],
|
|
[
|
|
"an accessor header name",
|
|
() => ({
|
|
...baseRegistration(),
|
|
requestHeaders: [
|
|
Object.defineProperty({ value: "1" }, "name", {
|
|
enumerable: true,
|
|
get: () => "x-safe",
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
[
|
|
"a binding with an extra field",
|
|
() => ({
|
|
...baseRegistration(),
|
|
binding: { kind: "DOWNLOAD", resourceId: "r", injected: true },
|
|
}),
|
|
],
|
|
[
|
|
"a null binding",
|
|
() => ({ ...baseRegistration(), binding: null }),
|
|
],
|
|
];
|
|
|
|
for (const [label, build] of hostileRegistrations) {
|
|
it(`rejects ${label} as POLICY_REJECTED`, () => {
|
|
const vault = freshVault();
|
|
expect(vault.register(build() as never)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
vault.dispose();
|
|
});
|
|
}
|
|
|
|
it("does not observe a mutation of the issuer's object after registration", () => {
|
|
const vault = freshVault();
|
|
const registration = baseRegistration();
|
|
const registered = vault.register(registration as never);
|
|
expect(registered.ok).toBe(true);
|
|
if (!registered.ok) return;
|
|
|
|
registration.requestHeaders[0]!.name = "authorization";
|
|
registration.expiresAtEpochMs = NOW + 999_999;
|
|
|
|
const resolved = vault.resolve(registered.value);
|
|
expect(resolved.ok).toBe(true);
|
|
if (!resolved.ok) return;
|
|
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
|
|
"x-safe",
|
|
]);
|
|
expect(resolved.value.expiresAtEpochMs).toBe(NOW + 60_000);
|
|
vault.dispose();
|
|
});
|
|
});
|
|
|
|
it("does not fetch a presigned download until stream consumption", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const responsePayload = downloadCapabilityPayload(bytes);
|
|
let downloadFetches = 0;
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
|
|
if (String(input) === CONTROL_ENDPOINT) {
|
|
return jsonResponse(responsePayload);
|
|
}
|
|
downloadFetches += 1;
|
|
return downloadResponse(bytes.slice().buffer, responsePayload);
|
|
}) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const signal = new AbortController().signal;
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
// BT-PRE-01. open() performs no network I/O.
|
|
expect(downloadFetches).toBe(0);
|
|
|
|
for await (const chunk of opened.value.stream(signal)) {
|
|
expect(chunk.ok).toBe(true);
|
|
}
|
|
expect(downloadFetches).toBe(1);
|
|
opened.value.close();
|
|
});
|
|
|
|
it("closes an unused download source without network I/O", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const responsePayload = downloadCapabilityPayload(bytes);
|
|
let downloadFetches = 0;
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
|
|
if (String(input) === CONTROL_ENDPOINT) {
|
|
return jsonResponse(responsePayload);
|
|
}
|
|
downloadFetches += 1;
|
|
return downloadResponse(bytes.slice().buffer, responsePayload);
|
|
}) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const signal = new AbortController().signal;
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
|
|
opened.value.close();
|
|
// close() is idempotent and never starts the transfer.
|
|
opened.value.close();
|
|
expect(downloadFetches).toBe(0);
|
|
|
|
// A stream after close is one terminal conflict, still without fetching.
|
|
const results = [];
|
|
for await (const chunk of opened.value.stream(signal)) {
|
|
results.push(chunk);
|
|
}
|
|
expect(results).toMatchObject([
|
|
{ ok: false, error: { code: "CONFLICT" } },
|
|
]);
|
|
expect(downloadFetches).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "truncation",
|
|
body: new Uint8Array([1, 2]),
|
|
expectedCode: "INTEGRITY_FAILED",
|
|
},
|
|
{
|
|
name: "overrun",
|
|
body: new Uint8Array([1, 2, 3, 4]),
|
|
expectedCode: "INTEGRITY_FAILED",
|
|
},
|
|
])("closes $name as a terminal stream failure", async ({ body, expectedCode }) => {
|
|
const declared = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(declared);
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(body.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
const results = await collect(opened.value);
|
|
expect(results.at(-1)).toMatchObject({
|
|
ok: false,
|
|
error: { code: expectedCode },
|
|
});
|
|
const firstFailure = results.findIndex((result) => !result.ok);
|
|
expect(results.slice(firstFailure + 1)).toEqual([]);
|
|
});
|
|
|
|
it("closes native body errors without throwing across the port", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const failingBody = new ReadableStream<Uint8Array>({
|
|
pull(controller) {
|
|
controller.error(new DOMException("secret native detail", "NetworkError"));
|
|
},
|
|
});
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(failingBody, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
await expect(collect(opened.value)).resolves.toMatchObject([
|
|
{
|
|
ok: false,
|
|
error: {
|
|
code: "NOT_READABLE",
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("closes active abort and timeout without leaking native rejection", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const neverBody = () =>
|
|
new ReadableStream<Uint8Array>({ pull() {} });
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(neverBody(), payload),
|
|
) as unknown as typeof fetch;
|
|
const controller = new AbortController();
|
|
let harness = createHarness({ fetcher });
|
|
let issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: controller.signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
let opened = await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: controller.signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
const aborted = collect(opened.value, controller.signal);
|
|
controller.abort("user");
|
|
expect(await aborted).toMatchObject([
|
|
{ ok: false, error: { code: "ABORTED" } },
|
|
]);
|
|
|
|
let timeoutCallback: (() => void) | undefined;
|
|
const scheduler = {
|
|
setTimeout(callback: () => void) {
|
|
timeoutCallback = callback;
|
|
return 1;
|
|
},
|
|
clearTimeout() {},
|
|
};
|
|
harness = createHarness({ fetcher, scheduler });
|
|
issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
opened = await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
const timedOut = collect(opened.value);
|
|
timeoutCallback?.();
|
|
expect(await timedOut).toMatchObject([
|
|
{
|
|
ok: false,
|
|
error: {
|
|
code: "UNAVAILABLE",
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects an expired capability before data-plane fetch", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
let current = NOW;
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(bytes.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({
|
|
fetcher,
|
|
now: () => current,
|
|
});
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
current = Number(payload.expiresAtEpochMs) + 1;
|
|
expect(
|
|
await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "EXPIRED_RESOURCE",
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
});
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("rejects capabilities below the configured minimum remaining lifetime", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const nearExpiryPayload = downloadCapabilityPayload(bytes, {
|
|
expiresAtEpochMs: NOW + 999,
|
|
});
|
|
let fetcher = vi.fn(async () =>
|
|
jsonResponse(nearExpiryPayload),
|
|
) as unknown as typeof fetch;
|
|
let harness = createHarness({ fetcher });
|
|
expect(
|
|
await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "EXPIRED_RESOURCE",
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
});
|
|
|
|
const acceptedPayload = downloadCapabilityPayload(bytes, {
|
|
expiresAtEpochMs: NOW + 2_000,
|
|
});
|
|
let current = NOW;
|
|
fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(acceptedPayload)
|
|
: downloadResponse(bytes.slice().buffer, acceptedPayload),
|
|
) as unknown as typeof fetch;
|
|
harness = createHarness({
|
|
fetcher,
|
|
now: () => current,
|
|
});
|
|
const issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
current = NOW + 1_001;
|
|
expect(
|
|
await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "EXPIRED_RESOURCE",
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
});
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("closes malformed AbortSignal inputs at every public boundary", async () => {
|
|
const bytes = new Uint8Array([1, 2]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const uploadChecksum = sha256Hex(bytes);
|
|
const uploadPayload = uploadCapabilityPayload({
|
|
bytes,
|
|
checksum: uploadChecksum,
|
|
});
|
|
const fetcher = vi.fn(
|
|
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
if (String(input) === CONTROL_ENDPOINT) {
|
|
const request = JSON.parse(String(init?.body)) as {
|
|
method: string;
|
|
};
|
|
return jsonResponse(
|
|
request.method === "GET" ? payload : uploadPayload,
|
|
);
|
|
}
|
|
if (String(input) === DOWNLOAD_HREF) {
|
|
return downloadResponse(bytes.slice().buffer, payload);
|
|
}
|
|
return responseWithUrl(
|
|
new Response(null, {
|
|
status: 200,
|
|
headers: {
|
|
[POLICY_HEADER]: "v1",
|
|
"Content-Length": "0",
|
|
ETag: "\"part-etag-1\"",
|
|
},
|
|
}),
|
|
String(uploadPayload.href),
|
|
);
|
|
},
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
const malformed = {} as AbortSignal;
|
|
|
|
expect(
|
|
await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: malformed,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
expect(
|
|
await provider.issueUploadPart({
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
byteLength: bytes.byteLength,
|
|
checksumSha256: uploadChecksum,
|
|
mediaType: "application/octet-stream",
|
|
idempotencyKey: "part-attempt-1",
|
|
signal: malformed,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
|
|
const issuedDownload = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issuedDownload.ok).toBe(true);
|
|
if (!issuedDownload.ok) return;
|
|
expect(
|
|
await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issuedDownload.value,
|
|
signal: malformed,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issuedDownload.value,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
expect(await collect(opened.value, malformed)).toMatchObject([
|
|
{ ok: false, error: { code: "INVALID_INPUT" } },
|
|
]);
|
|
expect(await collect(opened.value)).toMatchObject([
|
|
{ ok: false, error: { code: "CONFLICT" } },
|
|
]);
|
|
|
|
const issuedUpload = await provider.issueUploadPart({
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
byteLength: bytes.byteLength,
|
|
checksumSha256: uploadChecksum,
|
|
mediaType: "application/octet-stream",
|
|
idempotencyKey: "part-attempt-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issuedUpload.ok).toBe(true);
|
|
if (!issuedUpload.ok) return;
|
|
expect(
|
|
await executor.uploadParts.put({
|
|
capability: issuedUpload.value,
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
byteLength: bytes.byteLength,
|
|
checksumSha256: uploadChecksum,
|
|
idempotencyKey: "part-attempt-1",
|
|
bytes,
|
|
signal: malformed,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
});
|
|
|
|
});
|