electron/company-backups.node-test.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { test } from "node:test";
import { rmSync } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createHash } from "node:crypto";
import { createCompanyBackups } from "./company-backups.mjs";
const PART_BYTES = 64 * 1024 * 1024;
const MAX_BYTES = 10 * 1024 * 1024 * 1024;
const MAGIC = Buffer.from("OMB-WORKSPACE-1\n");
const BACKUP_ID = "11111111-1111-4111-8111-111111111111";
const EXPORT_ID = "22222222-2222-4222-8222-222222222222";
const UPLOAD_ID = "33333333-3333-4333-8333-333333333333";
const R2_ORIGIN = `https://${"a".repeat(32)}.r2.cloudflarestorage.com`;
const PASSWORD = "fixture-only backup password";
const SUMMARY = { format: "openmaus.workspace-backup", version: 1, id: UPLOAD_ID, bytes: 4096, bots: 1, conversations: 2, messages: 3 };
const partEtag = partNumber => `"${partNumber.toString(16).padStart(32, "0")}"`;
// Synthetic opaque container bytes; the real encryption and replacement workflow is the server fixture's.
function archiveChunk(offset, length) {
const chunk = Buffer.alloc(length, 0xa5);
if (offset < MAGIC.length) MAGIC.copy(chunk, 0, offset, Math.min(MAGIC.length, offset + length));
return chunk;
}
function archiveHash(size, start = 0) {
const hash = createHash("sha256");
for (let offset = start; offset < start + size; offset += 65536) {
hash.update(archiveChunk(offset, Math.min(65536, start + size - offset)));
}
return hash.digest("hex");
}
function archiveStream(size, { transform, onRead, onCancel } = {}) {
let offset = 0;
return new ReadableStream({
pull(controller) {
if (offset >= size) return controller.close();
const length = Math.min(65536, size - offset);
let chunk = archiveChunk(offset, length);
if (transform) chunk = transform(chunk, offset);
offset += length;
onRead?.(length);
controller.enqueue(chunk);
},
cancel() { onCancel?.(); },
}, { highWaterMark: 0 });
}
async function inspectBody(body) {
const hash = createHash("sha256");
let size = 0;
let prefix = Buffer.alloc(0);
const chunks = typeof body === "string" || ArrayBuffer.isView(body) || body instanceof ArrayBuffer
? [typeof body === "string" ? Buffer.from(body) : body instanceof ArrayBuffer ? Buffer.from(body) : body]
: body instanceof Blob ? body.stream() : body;
for await (const value of chunks) {
const chunk = Buffer.from(value);
size += chunk.length;
hash.update(chunk);
if (prefix.length < 100) prefix = Buffer.concat([prefix, chunk.subarray(0, 100 - prefix.length)]);
}
return { size, sha256: hash.digest("hex"), prefix };
}
function jsonBody(options) {
return typeof options.body === "string" ? JSON.parse(options.body) : options.body;
}
async function fixture(t, overrides = {}) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omb-company-backups-test-"));
t.after(() => fs.rm(root, { recursive: true, force: true }));
await fs.chmod(root, 0o700);
await fs.writeFile(path.join(root, "keep.txt"), "unrelated temporary content");
await fs.mkdir(path.join(root, "unrelated"));
await fs.writeFile(path.join(root, "unrelated", "keep.txt"), "unrelated directory content");
const size = overrides.size ?? 4096;
const sha256 = archiveHash(size);
const localCalls = [], portalCalls = [], storageCalls = [], uploaded = [], progress = [];
let created;
const ready = () => ({ id: BACKUP_ID, status: "ready", sizeBytes: size, sha256, passwordRequired: false });
const f = {
root, size, sha256, localCalls, portalCalls, storageCalls, uploaded, progress, ready,
archiveResponse: (bytes = size, options = {}) => new Response(archiveStream(bytes, options), {
headers: { "content-type": "application/octet-stream", "content-length": String(bytes) },
}),
async clean() {
assert.deepEqual((await fs.readdir(root)).sort(), ["keep.txt", "unrelated"]);
assert.equal(await fs.readFile(path.join(root, "keep.txt"), "utf8"), "unrelated temporary content");
assert.equal(await fs.readFile(path.join(root, "unrelated", "keep.txt"), "utf8"), "unrelated directory content");
},
};
f.client = createCompanyBackups({
tempRoot: root,
availableBytes: overrides.availableBytes ?? (async () => 1_000_000_000_000),
allowLoopbackForTests: overrides.allowLoopbackForTests ?? false,
async localRequest(route, options = {}) {
localCalls.push({ route, options });
const result = await overrides.local?.(route, options, f);
if (result !== undefined) return result;
if (route === "/api/workspace-backup/export") return Response.json({ id: EXPORT_ID, bytes: size, summary: SUMMARY });
if (route === `/api/workspace-backup/download/${EXPORT_ID}`) return f.archiveResponse();
if (route === "/api/workspace-backup/upload") {
uploaded.push(await inspectBody(options.body));
return Response.json({ id: UPLOAD_ID });
}
if (route === "/api/workspace-backup/preview") return Response.json({ id: UPLOAD_ID, summary: SUMMARY });
throw new Error(`Unexpected local fixture request: ${route}`);
},
async portalRequest(route, options = {}) {
portalCalls.push({ route, options });
const result = await overrides.portal?.(route, options, f);
if (result !== undefined) return result;
if (route === "/api/desktop/backups") {
created = jsonBody(options);
return { ...ready(), ...created, status: "uploading", partSizeBytes: PART_BYTES, partCount: Math.ceil(size / PART_BYTES) };
}
const part = route.match(new RegExp(`^/api/desktop/backups/${BACKUP_ID}/parts/(\\d+)$`));
if (part) {
const partNumber = Number(part[1]);
const sizeBytes = Math.min(PART_BYTES, size - (partNumber - 1) * PART_BYTES);
return { url: `${R2_ORIGIN}/fixture/object?partNumber=${partNumber}&signature=fixture`, headers: { "content-length": String(sizeBytes) }, sizeBytes, partNumber, expiresAt: Date.now() + 300_000 };
}
if (route === `/api/desktop/backups/${BACKUP_ID}/complete`) return { ...ready(), ...created, status: "ready" };
if (route === `/api/desktop/backups/${BACKUP_ID}/abort`) return { ok: true };
if (route === `/api/desktop/backups/${BACKUP_ID}/download`) return { ...ready(), unlockKey: PASSWORD, url: `${R2_ORIGIN}/fixture/object?signature=fixture`, expiresAt: Date.now() + 300_000 };
throw new Error(`Unexpected portal fixture request: ${route}`);
},
async fetchImpl(input, options = {}) {
const url = String(input);
storageCalls.push({ url, options });
const result = await overrides.storage?.(url, options, f);
if (result !== undefined) return result;
assert.equal(new URL(url).origin, R2_ORIGIN, "No real network is permitted by this fixture");
if (options.method === "PUT") {
const partNumber = Number(new URL(url).searchParams.get("partNumber"));
uploaded.push({ partNumber, ...await inspectBody(options.body) });
return new Response(null, { status: 200, headers: { etag: partEtag(partNumber) } });
}
assert.equal(options.method ?? "GET", "GET");
return f.archiveResponse();
},
});
f.backup = (signal, input = {}) => f.client.backup(input, signal, value => progress.push(value));
f.restore = signal => f.client.prepareRestore({ id: BACKUP_ID }, signal, value => progress.push(value));
return f;
}
function assertStorageIsolation(calls) {
for (const { options } of calls) {
assert.equal(options.redirect, "error");
assert.equal(options.credentials, "omit");
const headers = new Headers(options.headers);
assert.equal(headers.has("authorization"), false);
assert.equal(headers.has("cookie"), false);
}
}
test("backup generates a native key, sends it only to Admin, and never exposes it in returned metadata or progress", async t => {
const f = await fixture(t);
const clientState = { fixtureDraft: "private local draft" };
const ready = await f.backup(undefined, { clientState, appVersion: "0.0.0-fixture" });
assert.equal(ready.id, BACKUP_ID);
assert.equal(ready.status, "ready");
const password = jsonBody(f.localCalls[0].options).password;
assert.match(password, /^[A-Za-z0-9_-]{43}$/);
assert.deepEqual(jsonBody(f.localCalls[0].options), { password, clientState });
const create = f.portalCalls.find(call => call.route === "/api/desktop/backups");
assert.deepEqual(jsonBody(create.options), { sizeBytes: f.size, sha256: f.sha256, appVersion: "0.0.0-fixture", unlockKey: password });
assert(!JSON.stringify(ready).includes(password));
assert(!JSON.stringify(f.progress).includes(password));
assert(!JSON.stringify(f.storageCalls).includes(password));
assert.equal(f.uploaded.length, 1);
assert.equal(f.uploaded[0].size, f.size);
assert.equal(f.uploaded[0].sha256, f.sha256);
assert.deepEqual(f.uploaded[0].prefix.subarray(0, MAGIC.length), MAGIC);
const complete = f.portalCalls.find(call => call.route.endsWith("/complete"));
assert.deepEqual(jsonBody(complete.options), { parts: [{ partNumber: 1, etag: partEtag(1) }] });
assert(!JSON.stringify(f.portalCalls).includes(PASSWORD));
assert(!JSON.stringify(f.portalCalls).includes(clientState.fixtureDraft));
assert(!JSON.stringify(f.progress).includes(PASSWORD));
assert(f.progress.length > 0);
assertStorageIsolation(f.storageCalls);
await f.clean();
});
test("multipart backup streams exact 64 MiB boundaries without collecting the archive in the test", async t => {
const f = await fixture(t, { size: PART_BYTES + 713 });
await f.backup();
assert.deepEqual(f.uploaded.map(({ partNumber, size, sha256 }) => ({ partNumber, size, sha256 })), [
{ partNumber: 1, size: PART_BYTES, sha256: archiveHash(PART_BYTES) },
{ partNumber: 2, size: 713, sha256: archiveHash(713, PART_BYTES) },
]);
const complete = f.portalCalls.find(call => call.route.endsWith("/complete"));
assert.deepEqual(jsonBody(complete.options).parts, [
{ partNumber: 1, etag: partEtag(1) },
{ partNumber: 2, etag: partEtag(2) },
]);
assertStorageIsolation(f.storageCalls);
await f.clean();
});
test("older Admin cannot silently accept an archive without retaining its automatic key", async t => {
const f = await fixture(t, { portal: route => route === "/api/desktop/backups" ? {
id: BACKUP_ID, status: "uploading", sizeBytes: 4096, sha256: archiveHash(4096), partSizeBytes: PART_BYTES, partCount: 1,
} : undefined });
await assert.rejects(f.backup(), { code: "update_required" });
assert.equal(f.storageCalls.length, 0);
assert(f.portalCalls.some(call => call.route.endsWith("/abort")));
});
test("legacy password archives still restore, while managed archives require the service key", async t => {
const f = await fixture(t, { portal: (route, _options, state) => route.endsWith("/download") ? {
...state.ready(), passwordRequired: true, url: `${R2_ORIGIN}/fixture/object`, expiresAt: Date.now() + 300_000,
} : undefined });
await assert.rejects(f.restore(), { code: "invalid_password" });
await f.client.prepareRestore({ id: BACKUP_ID, password: PASSWORD });
assert.equal(jsonBody(f.localCalls.at(-1).options).password, PASSWORD);
const missing = await fixture(t, { portal: (route, _options, state) => route.endsWith("/download") ? {
...state.ready(), url: `${R2_ORIGIN}/fixture/object`, expiresAt: Date.now() + 300_000,
} : undefined });
await assert.rejects(missing.client.prepareRestore({ id: BACKUP_ID, password: PASSWORD }), { code: "invalid_password" });
assert.equal(missing.storageCalls.length, 0);
});
test("restore verifies the downloaded archive and prepares only a local preview, never replacement", async t => {
const f = await fixture(t);
const preview = await f.restore();
assert.deepEqual(preview, { id: UPLOAD_ID, summary: SUMMARY });
assert.equal(f.uploaded.length, 1);
assert.equal(f.uploaded[0].size, f.size);
assert.equal(f.uploaded[0].sha256, f.sha256);
assert.deepEqual(f.localCalls.map(call => call.route), ["/api/workspace-backup/upload", "/api/workspace-backup/preview"]);
assert.deepEqual(jsonBody(f.localCalls[1].options), { id: UPLOAD_ID, password: PASSWORD });
assert.equal(new Headers(f.localCalls[0].options.headers).get("content-type"), "application/octet-stream");
assert(!JSON.stringify(f.portalCalls).includes(PASSWORD));
assert(!f.localCalls.some(call => /\/restore(?:\/|$)/.test(call.route)));
assertStorageIsolation(f.storageCalls);
await f.clean();
});
for (const corruption of ["short", "long", "hash", "magic"]) {
test(`restore rejects ${corruption} download before any local upload or preview`, async t => {
const f = await fixture(t, {
storage: (_url, _options, state) => {
if (corruption === "short") return state.archiveResponse(state.size - 1);
if (corruption === "long") return state.archiveResponse(state.size + 1);
return state.archiveResponse(state.size, { transform: (chunk, offset) => {
if (offset === 0) chunk[corruption === "magic" ? 0 : 100] ^= 1;
return chunk;
} });
},
});
await assert.rejects(f.restore());
assert.deepEqual(f.localCalls, []);
await f.clean();
});
}
for (const bytes of [59, MAX_BYTES + 1, -1, 2.5]) {
test(`invalid export size ${bytes} is rejected before fetching bytes or creating cloud state`, async t => {
const f = await fixture(t, {
local: route => route.endsWith("/export") ? Response.json({ id: EXPORT_ID, bytes, summary: SUMMARY }) : undefined,
});
await assert.rejects(f.backup());
assert.equal(f.localCalls.length, 1);
assert.deepEqual(f.portalCalls, []);
assert.deepEqual(f.storageCalls, []);
await f.clean();
});
}
test("local export JSON responses are bounded before parsing", async t => {
let readBytes = 0;
let cancelled = false;
const f = await fixture(t, {
local: route => route.endsWith("/export") ? new Response(archiveStream(8 * 1024 * 1024, {
transform: chunk => Buffer.alloc(chunk.length, 0x20),
onRead: bytes => { readBytes += bytes; }, onCancel: () => { cancelled = true; },
}), { headers: { "content-type": "application/json" } }) : undefined,
});
await assert.rejects(f.backup());
assert(readBytes < 8 * 1024 * 1024, "Stop reading oversized JSON before its entire body arrives");
assert.equal(cancelled, true);
assert.deepEqual(f.portalCalls, []);
await f.clean();
});
test("a local archive that exceeds its declared size is rejected before cloud creation", async t => {
const f = await fixture(t, {
local: (route, _options, state) => route.includes("/download/") ? new Response(archiveStream(state.size + 1)) : undefined,
});
await assert.rejects(f.backup());
assert.deepEqual(f.portalCalls, []);
await f.clean();
});
for (const url of [
"https://attacker.example.test/object",
`${R2_ORIGIN}.attacker.example.test/object`,
"https://r2.cloudflarestorage.com/object",
`${R2_ORIGIN.replace("https:", "http:")}/object`,
`https://user:password@${new URL(R2_ORIGIN).host}/object`,
`${R2_ORIGIN}:444/object`,
"http://127.0.0.1:9987/object",
"file:///tmp/fixture.ombbackup",
]) {
test(`cloud restore refuses untrusted storage URL ${url}`, async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route.endsWith("/download") ? { ...state.ready(), url, expiresAt: Date.now() + 300_000 } : undefined,
});
await assert.rejects(f.restore());
assert.deepEqual(f.storageCalls, []);
assert.deepEqual(f.localCalls, []);
await f.clean();
});
}
test("an invalid signed upload URL triggers an abort without fetching that URL", async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route.includes("/parts/") ? {
url: "https://attacker.example.test/upload", headers: { "content-length": String(state.size) },
sizeBytes: state.size, partNumber: 1, expiresAt: Date.now() + 300_000,
} : undefined,
});
await assert.rejects(f.backup());
assert.deepEqual(f.storageCalls, []);
assert(f.portalCalls.some(call => call.route.endsWith("/abort")));
assert(!f.portalCalls.some(call => call.route.endsWith("/complete")));
await f.clean();
});
for (const [name, mutation] of [
["expired download", { expiresAt: 1 }],
["non-ready download", { status: "uploading" }],
["different backup ID", { id: EXPORT_ID }],
["oversized archive", { sizeBytes: MAX_BYTES + 1 }],
["undersized archive", { sizeBytes: 59 }],
["invalid SHA-256", { sha256: "not-a-checksum" }],
]) {
test(`restore rejects ${name} metadata without fetching storage`, async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route.endsWith("/download") ? {
...state.ready(), url: `${R2_ORIGIN}/fixture/object`, expiresAt: Date.now() + 300_000, ...mutation,
} : undefined,
});
await assert.rejects(f.restore());
assert.deepEqual(f.storageCalls, []);
assert.deepEqual(f.localCalls, []);
await f.clean();
});
}
for (const [name, mutation] of [
["expired signature", { expiresAt: 1 }],
["different part number", { partNumber: 2 }],
["different part size", { sizeBytes: 4000 }],
["different content length", { headers: { "content-length": "4000" } }],
]) {
test(`upload rejects ${name} before sending bytes`, async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route.includes("/parts/") ? {
url: `${R2_ORIGIN}/fixture/object?partNumber=1`, headers: { "content-length": String(state.size) },
sizeBytes: state.size, partNumber: 1, expiresAt: Date.now() + 300_000, ...mutation,
} : undefined,
});
await assert.rejects(f.backup());
assert.deepEqual(f.storageCalls, []);
assert(f.portalCalls.some(call => call.route.endsWith("/abort")));
assert(!f.portalCalls.some(call => call.route.endsWith("/complete")));
await f.clean();
});
}
test("the portal cannot select a different multipart chunk size", async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route === "/api/desktop/backups" ? {
...state.ready(), status: "uploading", partSizeBytes: 128 * 1024 * 1024, partCount: 1,
} : undefined,
});
await assert.rejects(f.backup());
assert.deepEqual(f.storageCalls, []);
assert(!f.portalCalls.some(call => call.route.includes("/parts/") || call.route.endsWith("/complete")));
await f.clean();
});
test("signed storage metadata cannot inject authorization or cookie headers into uploads", async t => {
const f = await fixture(t, {
portal: (route, _options, state) => route.includes("/parts/") ? {
url: `${R2_ORIGIN}/fixture/object?partNumber=1`,
headers: { "content-length": String(state.size), authorization: "Bearer fixture-secret", cookie: "session=fixture-secret" },
sizeBytes: state.size, partNumber: 1, expiresAt: Date.now() + 300_000,
} : undefined,
});
await assert.rejects(f.backup());
assert.deepEqual(f.storageCalls, []);
assert(f.portalCalls.some(call => call.route.endsWith("/abort")));
await f.clean();
});
for (const operation of ["backup", "restore"]) {
test(`${operation} does not follow storage redirects or forward authority`, async t => {
const f = await fixture(t, {
storage: () => new Response(null, { status: 307, headers: { location: "https://attacker.example.test/steal" } }),
});
await assert.rejects(f[operation]());
assert.equal(f.storageCalls.length, 1);
assertStorageIsolation(f.storageCalls);
assert(!f.storageCalls.some(call => call.url.includes("attacker")));
assert(!f.localCalls.some(call => call.route.endsWith("/upload") || call.route.endsWith("/preview")));
if (operation === "backup") assert(f.portalCalls.some(call => call.route.endsWith("/abort")));
await f.clean();
});
}
for (const operation of ["backup", "restore"]) {
test(`an unread ${operation} upload body whose file is gone before it opens leaves no uncaught stream error`, async t => {
const unread = (_, options) => { rmSync(options.body.path); return new Response(null, { status: 500 }); };
const f = await fixture(t, operation === "backup"
? { storage: unread }
: { local: (route, options) => route === "/api/workspace-backup/upload" ? unread(route, options) : undefined });
const uncaught = [];
const record = error => uncaught.push(error);
process.on("uncaughtException", record);
try {
await assert.rejects(f[operation](), { code: operation === "backup" ? "upload_failed" : "local_backup_failed" });
await new Promise(resolve => setTimeout(resolve, 50));
} finally { process.off("uncaughtException", record); }
assert.deepEqual(uncaught, []);
if (operation === "restore") assert(f.localCalls.some(call => call.route.endsWith("/upload")));
await f.clean();
});
}
test("cancelled upload attempts a fresh-signal best-effort abort even if portal cleanup fails", async t => {
const controller = new AbortController();
const f = await fixture(t, {
storage: async (_url, options) => {
await inspectBody(options.body);
controller.abort();
throw new DOMException("The operation was aborted", "AbortError");
},
portal: (route, options) => {
if (route.endsWith("/abort")) {
assert.notEqual(options.signal?.aborted, true, "Cleanup must not reuse the cancelled transfer signal");
throw new Error("Fixture portal temporarily unavailable");
}
},
});
await assert.rejects(f.backup(controller.signal));
assert.equal(f.portalCalls.filter(call => call.route.endsWith("/abort")).length, 1);
assert(!f.portalCalls.some(call => call.route.endsWith("/complete")));
await f.clean();
});
test("an already-cancelled operation makes no local or portal requests", async t => {
const f = await fixture(t);
const controller = new AbortController();
controller.abort();
await assert.rejects(f.backup(controller.signal));
await assert.rejects(f.restore(controller.signal));
assert.deepEqual(f.localCalls, []);
assert.deepEqual(f.portalCalls, []);
assert.deepEqual(f.storageCalls, []);
await f.clean();
});
for (const operation of ["backup", "restore"]) {
test(`${operation} fails safely when free temporary storage is insufficient`, async t => {
const f = await fixture(t, { availableBytes: async () => 0 });
await assert.rejects(f[operation]());
assert.deepEqual(f.storageCalls, []);
assert(!f.localCalls.some(call => call.route.includes("/download/") || call.route.endsWith("/upload") || call.route.endsWith("/preview")));
assert(!f.portalCalls.some(call => call.route === "/api/desktop/backups"));
await f.clean();
});
}
test("space is rechecked after export size is known and before downloading its bytes", async t => {
const f = await fixture(t, { availableBytes: async () => PART_BYTES + 100 });
await assert.rejects(f.backup());
assert.equal(f.localCalls.filter(call => call.route.endsWith("/export")).length, 1);
assert(!f.localCalls.some(call => call.route.includes("/download/")));
assert.deepEqual(f.portalCalls, []);
await f.clean();
});
for (const [stage, checkNumber, multiplier, localUploads, storageRequests] of [
["download", 2, 4, 0, 0],
["local upload", 3, 3, 0, 1],
["preview", 4, 2, 1, 1],
]) {
test(`restore reserves simultaneous archive copies before ${stage}`, async t => {
let checks = 0;
const f = await fixture(t, {
availableBytes: async () => ++checks === checkNumber ? PART_BYTES + 4096 * multiplier - 1 : 1_000_000_000_000,
});
await assert.rejects(f.restore(), { code: "no_space" });
assert.equal(f.storageCalls.length, storageRequests);
assert.equal(f.localCalls.filter(call => call.route.endsWith("/upload")).length, localUploads);
assert(!f.localCalls.some(call => call.route.endsWith("/preview")));
await f.clean();
});
}
for (const [stage, limit] of [["upload", 4096], ["preview", 2 * 1024 * 1024]]) {
test(`restore bounds the local ${stage} JSON response and cancels its oversized stream`, async t => {
const totalBytes = Math.max(limit * 4, 1024 * 1024);
let readBytes = 0;
let cancelled = false;
const f = await fixture(t, {
local: async (route, options) => {
if (!route.endsWith(`/${stage}`)) return;
if (stage === "upload") await inspectBody(options.body);
return new Response(archiveStream(totalBytes, {
transform: chunk => Buffer.alloc(chunk.length, 0x20),
onRead: bytes => { readBytes += bytes; }, onCancel: () => { cancelled = true; },
}), { headers: { "content-type": "application/json" } });
},
});
await assert.rejects(f.restore(), { code: "invalid_response" });
// Stream adapters may prefetch a chunk, but cannot consume the entire body.
assert(readBytes < totalBytes);
assert.equal(cancelled, true);
if (stage === "upload") assert(!f.localCalls.some(call => call.route.endsWith("/preview")));
assert(!f.localCalls.some(call => /\/restore(?:\/|$)/.test(call.route)));
await f.clean();
});
}
test("restore cancellation while reading storage never sends local upload or preview", async t => {
const controller = new AbortController();
const f = await fixture(t, {
storage: (_url, _options, state) => state.archiveResponse(state.size, { onRead: () => controller.abort() }),
});
await assert.rejects(f.restore(controller.signal), { code: "cancelled" });
assert.deepEqual(f.localCalls, []);
await f.clean();
});
test("temporary operation files and directories are private while upload is in progress", async t => {
const f = await fixture(t, {
storage: async (_url, options, state) => {
const entries = await fs.readdir(state.root, { withFileTypes: true });
const owned = entries.filter(entry => !["keep.txt", "unrelated"].includes(entry.name));
assert(owned.length > 0, "Expected an operation-private staging directory");
for (const entry of owned) {
const target = path.join(state.root, entry.name);
const stat = await fs.lstat(target);
assert.equal(stat.isSymbolicLink(), false);
if (process.platform !== "win32") assert.equal(stat.mode & 0o077, 0);
if (entry.isDirectory()) {
for (const child of await fs.readdir(target)) {
const childStat = await fs.lstat(path.join(target, child));
if (process.platform !== "win32") assert.equal(childStat.mode & 0o077, 0);
}
}
}
await inspectBody(options.body);
return new Response(null, { headers: { etag: partEtag(1) } });
},
});
await f.backup();
await f.clean();
});
test("progress callback exceptions cannot prevent successful transfer or private cleanup", async t => {
const f = await fixture(t);
const ready = await f.client.backup({ password: PASSWORD }, undefined, () => { throw new Error("Fixture UI closed"); });
assert.equal(ready.status, "ready");
await f.clean();
});