Files
Termix/src/backend/tests/plugins/user-data.test.ts
T

138 lines
4.2 KiB
TypeScript

/**
* A user's rows in plugin tables travel with their export and come back on
* import, found through the table definitions plugins registered rather than
* any table name core knows.
*/
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
defineTable,
encryptedText,
id,
refUser,
text,
varchar,
} from "@termix/plugin-sdk/db";
let sqlite: Database.Database;
vi.mock("../../database/db/index.js", () => ({
getDb: () => drizzle(sqlite),
}));
vi.mock("../../utils/logger.js", () => ({
pluginLogger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
const { registerTable, resetPluginData } =
await import("../../plugins/data.js");
const {
importUserPluginRows,
listUserOwnedTables,
readUserPluginRows,
writeUserPluginTables,
} = await import("../../plugins/user-data.js");
const { createTableSql } = await import("../../plugins/table-builder.js");
// Stands for an adopted table; adoptLegacyTable only accepts real core names.
const bookmarks = {
...defineTable("bookmark", {
id: id(),
userId: refUser(),
path: varchar(200).notNull(),
note: text(),
token: encryptedText(),
}),
adopts: "bookmarks_legacy",
};
const shared = defineTable("shared_thing", { id: id(), label: text() });
function createTables(target: Database.Database) {
target.exec("CREATE TABLE users (id TEXT PRIMARY KEY)");
target.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY)");
for (const definition of [bookmarks, shared]) {
for (const statement of createTableSql("sqlite", "fixture", definition)) {
target.exec(statement);
}
}
}
beforeEach(() => {
sqlite = new Database(":memory:");
createTables(sqlite);
sqlite.exec("INSERT INTO users (id) VALUES ('u1'), ('u2')");
sqlite.exec(`INSERT INTO p_fixture_bookmark (user_id, path, note, token)
VALUES ('u1', '/etc', 'config', 'sealed'), ('u2', '/var', null, null)`);
registerTable("fixture", bookmarks);
registerTable("fixture", shared);
});
afterEach(() => {
resetPluginData();
sqlite.close();
});
describe("plugin user data", () => {
it("lists only tables with a user column, without keys or secrets", () => {
const [owned] = listUserOwnedTables();
expect(listUserOwnedTables()).toHaveLength(1);
expect(owned).toMatchObject({
pluginId: "fixture",
table: "p_fixture_bookmark",
legacyName: "bookmarks_legacy",
userColumn: "user_id",
columns: ["user_id", "path", "note"],
});
});
it("reads one user's rows", async () => {
expect(await readUserPluginRows("u1")).toEqual({
p_fixture_bookmark: [{ user_id: "u1", path: "/etc", note: "config" }],
});
});
it("round-trips through an export file into another user", async () => {
// The export file has the user row, as database.ts writes it first.
const file = new Database(":memory:");
file.exec("CREATE TABLE users (id TEXT PRIMARY KEY)");
file.exec("CREATE TABLE ssh_data (id INTEGER PRIMARY KEY)");
file.exec("INSERT INTO users (id) VALUES ('u1')");
expect(await writeUserPluginTables(file, "u1")).toBe(1);
sqlite.exec("DELETE FROM p_fixture_bookmark");
const result = await importUserPluginRows(file, "u2");
expect(result).toEqual({ imported: 1, skipped: 0, errors: [] });
expect(
sqlite.prepare("SELECT user_id, path FROM p_fixture_bookmark").all(),
).toEqual([{ user_id: "u2", path: "/etc" }]);
const again = await importUserPluginRows(file, "u2");
expect(again).toMatchObject({ imported: 0, skipped: 1 });
});
it("reads a pre-2.9 export by the legacy table name", async () => {
const file = new Database(":memory:");
file.exec(
"CREATE TABLE bookmarks_legacy (id INTEGER PRIMARY KEY, user_id TEXT, path TEXT, note TEXT)",
);
file.exec(
"INSERT INTO bookmarks_legacy (user_id, path, note) VALUES ('old', '/srv', 'x')",
);
const result = await importUserPluginRows(file, "u1");
expect(result.imported).toBe(1);
expect(
sqlite
.prepare(
"SELECT user_id, note FROM p_fixture_bookmark WHERE path = '/srv'",
)
.get(),
).toEqual({ user_id: "u1", note: "x" });
});
});