// What automx_rows reads (novox/hq issue 1630): the rows of automx2's database as stored, through a read-only // sqlite3 inside its container. The database here is a real SQLite file made by the test with the tables // automx/files/setup-db creates and the rows add-domains writes — with the IMAP server's name empty, the case // the live instance is in. The fake container runs each command's statement against that database and prints // it the way the image's sqlite3 does in list mode (one line per row, cells apart by "|", nothing for no rows); that sqlite3 // refuses `-json` ("sqlite3: Error: unknown option: -json", captured from novox/mailu.automx_rows, issue 1630). Run against the compiled module. import { test } from "node:test"; import assert from "node:assert/strict"; import { DatabaseSync } from "node:sqlite"; import { readAutomx, readCommand, selectOf, parseRows, AUTOMX_DATABASE } from "../dist/automx.js"; function database(imapName: string): DatabaseSync { const db = new DatabaseSync(":memory:"); db.exec(` CREATE TABLE provider(id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, short_name TEXT NOT NULL); CREATE TABLE server(id INT PRIMARY KEY NOT NULL, prio INT NOT NULL DEFAULT 10, name TEXT NOT NULL, port INT NOT NULL, type TEXT NOT NULL, socket_type TEXT NOT NULL, user_name TEXT NOT NULL, authentication TEXT NOT NULL); CREATE TABLE domain(id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, provider_id INT NOT NULL, ldapserver_id INT NULL); CREATE TABLE server_domain(server_id INT NOT NULL, domain_id INT NOT NULL); INSERT INTO provider VALUES (1, 'example.org', 'example.org'); INSERT INTO server(id, port, type, name, socket_type, user_name, authentication) VALUES (2, 143, 'imap', '${imapName}', 'STARTTLS', '%EMAILADDRESS%', 'password-cleartext'); INSERT INTO server(id, port, type, name, socket_type, user_name, authentication) VALUES (3, 587, 'smtp', 'mail.example.org', 'STARTTLS', '%EMAILADDRESS%', 'password-cleartext'); INSERT INTO domain VALUES (5, 'example.org', 1, NULL); INSERT INTO server_domain VALUES (2, 5), (3, 5); `); return db; } function container(db: DatabaseSync, asked: string[][] = []) { return async (_name: string, command: string[]) => { asked.push(command); if (command.includes("-json")) throw Object.assign(new Error("Command failed: docker exec"), { stderr: "sqlite3: Error: unknown option: -json\n" }); const rows = db.prepare(command[command.length - 1]).all() as unknown as Record[]; return rows.length === 0 ? "" : rows.map((r) => Object.values(r).join("|")).join("\n") + "\n"; }; } test("the rows come back as stored, and an empty server name is said", async () => { const answer = await readAutomx(container(database("")), "mailu-automx"); assert.equal(answer.database, AUTOMX_DATABASE); assert.deepEqual(answer.unreadable, {}); assert.deepEqual(answer.tables.provider, [{ id: 1, name: "example.org", short_name: "example.org" }]); assert.deepEqual(answer.tables.domain, [{ id: 5, name: "example.org", provider_id: 1, ldapserver_id: null }]); assert.deepEqual(answer.tables.server_domain, [{ server_id: 2, domain_id: 5 }, { server_id: 3, domain_id: 5 }]); assert.deepEqual( answer.tables.server.map((s) => [s.id, s.type, s.name, s.port]), [[2, "imap", "", 143], [3, "smtp", "mail.example.org", 587]], ); assert.deepEqual(answer.empty_names, [{ id: 2, type: "imap" }]); }); test("a named IMAP server is not reported empty", async () => { const answer = await readAutomx(container(database("mail.example.org")), "mailu-automx"); assert.deepEqual(answer.empty_names, []); }); test("every command opens the file read-only and runs one fixed SELECT", async () => { const asked: string[][] = []; await readAutomx(container(database("x"), asked), "mailu-automx"); assert.equal(asked.length, 4); for (const c of asked) { assert.deepEqual(c.slice(0, 3), ["sqlite3", "-readonly", AUTOMX_DATABASE]); assert.match(c[3], /^SELECT /); assert.equal(c.length, 4); assert.ok(!c.includes("-json")); } assert.deepEqual(readCommand("provider"), ["sqlite3", "-readonly", AUTOMX_DATABASE, selectOf("provider")]); }); test("a table that cannot be read is said and does not hide the others", async () => { const db = database("x"); const exec = async (name: string, command: string[]) => { if (command[3].includes("FROM domain ")) throw Object.assign(new Error("Command failed: docker exec"), { stderr: "Error: unable to open database file\nmore" }); return container(db)(name, command); }; const answer = await readAutomx(exec, "mailu-automx"); assert.deepEqual(answer.unreadable, { domain: "Error: unable to open database file" }); assert.equal(answer.tables.provider.length, 1); assert.equal(answer.tables.domain, undefined); }); test("a NULL, an empty text, a number and a text holding a bar or a newline are read apart", () => { const hex = (t: string) => Buffer.from(t).toString("hex").toUpperCase(); const line = [`integer:${hex("5")}`, `text:${hex("a|b\nc")}`, "text:", "null:"].join("|"); const rows = parseRows("domain", line + "\n"); assert.deepEqual(rows, [{ id: 5, name: "a|b\nc", provider_id: "", ldapserver_id: null }]); }); test("output that is not cells is an unreadable table, not a guess", async () => { const exec = async () => "Error: something else\n"; const answer = await readAutomx(exec, "mailu-automx"); assert.equal(Object.keys(answer.unreadable).length, 4); }); test("the image's refusal of -json is said as the table's reason", async () => { const exec = async (_n: string, command: string[]) => container(database("x"))("", ["sqlite3", "-json", ...command.slice(1)]); const answer = await readAutomx(exec, "mailu-automx"); assert.deepEqual(answer.unreadable.domain, "sqlite3: Error: unknown option: -json"); });