check / check (push) Successful in 3m29s
`quak login` takes the server's API address from `--server` or `QUAK_SERVER`. `isServerAddress` refuses anything but an http or https URL with no query or fragment, there and in `Client.fromJSON`. The client's snapshot keeps a non-default `apiOrigin`, so the session file names the server and every later command uses it. Two servers can give two accounts, and two files, the same ID. A self-hosted account's default cache directory therefore names the server, and `metadata.json` records it: `Library.open` deletes a cache from another server whole. `originalOf` and `thumbnailOf` record the account in a cache that names none before storing a file, and `open` treats a cache with no collection as empty. This fixes #254 (closes #254). Model: opus-5-5
152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
/**
|
|
* 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` and sending its first request to the server it names.
|
|
*
|
|
* 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. `quak login` reads no
|
|
* session, so its test stubs `fetch` instead and records each 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 <args>` 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<string> => {
|
|
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();
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
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'/,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("bin/quak.ts login", () => {
|
|
// The command refuses an address that is not a URL before it asks for
|
|
// anything or sends any request.
|
|
it("hands --server to the command", async () => {
|
|
expect(await quak("login", "--server", "ente.example.com")).toMatch(
|
|
/^Invalid server address "ente\.example\.com"/,
|
|
);
|
|
});
|
|
|
|
// `fetch` answers 404, so the login fails at its first request. A login
|
|
// that succeeded would save its session in this user's real data
|
|
// directory, since env-paths is not mocked here.
|
|
it("sends its first request to the server --server names", async () => {
|
|
const requests: string[] = [];
|
|
vi.stubGlobal("fetch", async (input: string) => {
|
|
const { origin, pathname } = new URL(input);
|
|
requests.push(origin + pathname);
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
vi.stubEnv("QUAK_EMAIL", "login@example.com");
|
|
vi.stubEnv("QUAK_PASSWORD", "password");
|
|
await quak("login", "--server", "https://ente.example.com");
|
|
expect(requests).toEqual([
|
|
"https://ente.example.com/users/srp/attributes",
|
|
]);
|
|
});
|
|
});
|