Ctrl K
ComunidadGuías…

electron/company-backups.node-test.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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();
});