/** * Tests for the retry options `bin/quak.ts` loads the saved session with * (`quak backup` gets the unattended ones, every other command the default), * each with the callbacks that log its requests and retries, for `quak files` * taking its collection ID as an optional argument, for `quak get` and * `quak get-thumb` refusing `--collection`, and for `quak login` taking * `--server`. * * Each test runs `bin/quak.ts`, as the smoke test does, with its session loader * replaced by one that records the options it is given and reports no session, * so the command stops before it makes any request. * * The first load of the sources `bin/quak.ts` imports compiles them, and the * first `init()` starts libsodium. On a loaded host that takes longer than a * test's timeout, so both happen here, before the tests run. Each test's * `vi.resetModules()` then only runs the compiled sources again. */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { ApiClientOptions } from "../../src/api/client.js"; import "../../src/cli-commands.js"; import { init } from "../../src/crypto/index.js"; import "../../src/index.js"; import { UNATTENDED_RETRY_OPTIONS } from "../../src/retry.js"; await init(); const loaded = vi.hoisted(() => [] as (ApiClientOptions | undefined)[]); vi.mock("../../src/cli-session.js", () => ({ loadSession: (_path: string, apiOptions?: ApiClientOptions) => { loaded.push(apiOptions); return null; }, })); const argv = process.argv; const dir = mkdtempSync(join(tmpdir(), "quak-bin-test-")); // Run `quak ` to completion and return what it wrote on stderr, which is // kept off the terminal. The exit is swallowed. const quak = async (...args: string[]): Promise => { let written = ""; vi.spyOn(process.stderr, "write").mockImplementation((text) => { written += String(text); return true; }); vi.spyOn(process, "exit").mockImplementation(() => undefined as never); process.argv = ["node", "quak", ...args]; vi.resetModules(); await import("../../bin/quak.js"); return written; }; afterEach(() => { process.argv = argv; loaded.length = 0; vi.restoreAllMocks(); }); afterAll(() => { rmSync(dir, { recursive: true, force: true }); }); describe("bin/quak.ts session loading", () => { it("loads the session for backup with the unattended retry options", async () => { await quak("backup", dir); // `vi.resetModules()` gave `bin/quak.ts` its own copy of // `src/retry.ts`, whose `sleep` is a different function, so the // options are compared by their numbers. const { attempts, baseDelayMs, maxDelayMs } = UNATTENDED_RETRY_OPTIONS; expect(loaded).toEqual([ { retry: expect.objectContaining({ attempts, baseDelayMs, maxDelayMs, onRetry: expect.any(Function), }), onRequest: expect.any(Function), }, ]); }); it("loads the session for another command with the default retry options", async () => { await quak("collections"); expect(loaded).toEqual([ { retry: { onRetry: expect.any(Function) }, onRequest: expect.any(Function), }, ]); }); }); // The command line takes `files` with or without a collection ID, and the // command runs as far as its session check either way (issue #211). describe("bin/quak.ts files", () => { it("runs without a collection ID", async () => { expect(await quak("files")).toMatch(/^Not logged in/); }); it("runs with a collection ID", async () => { expect(await quak("files", "7")).toMatch(/^Not logged in/); }); }); describe("bin/quak.ts get and get-thumb", () => { it.each(["get", "get-thumb"])( "%s refuses --collection as an unknown option", async (command) => { expect(await quak(command, "1", "--collection", "2")).toMatch( /^error: unknown option '--collection'/, ); }, ); }); // The command refuses an address that is not a URL before it asks for anything // or sends any request. describe("bin/quak.ts login", () => { it("hands --server to the command", async () => { expect(await quak("login", "--server", "ente.example.com")).toMatch( /^Invalid server address "ente\.example\.com"/, ); }); });