Files

141 lines
4.3 KiB
JavaScript
Raw Permalink Normal View History

+3
2026-08-06 14:41:39 -05:00
/**
* Runs the repository layer against a real Postgres or MySQL server.
*
* The unit tests only ever see SQLite, so the parts of this codebase that
* differ per engine — the RETURNING replacements, the read-then-write
* transactions, the value encoders — have no coverage there at all. This is
* what covers them, and it needs a live server, which is why it is a script
* rather than a test.
*
* Usage:
* npm run verify:dialect -- postgres://user:pass@host:5432/db
* npm run verify:dialect -- mysql://user:pass@host:3306/db
*
* Applies the migrations first, through the same runRemoteMigrations() the
* application uses at startup — so a broken migration fails here rather than in
* production. Writes real rows: point it at a scratch database.
*/
import { randomUUID } from "crypto";
const url = process.argv[2];
if (!url) {
console.error("usage: node scripts/verify-dialects.mjs <DATABASE_URL>");
process.exit(2);
}
const scheme = url.split("://", 1)[0].toLowerCase();
const dialect = scheme.startsWith("postgres")
? "postgres"
: scheme === "mysql" || scheme === "mariadb"
? "mysql"
: null;
if (!dialect) {
console.error(`unsupported URL scheme "${scheme}://"`);
process.exit(2);
}
const { drizzle } = await import(
dialect === "postgres" ? "drizzle-orm/node-postgres" : "drizzle-orm/mysql2"
);
// No schema option on purpose — see connect.ts.
const db = drizzle(url);
const context = { dialect, drizzle: db };
const { runRemoteMigrations } =
await import("../src/backend/database/db/migrate.js");
await runRemoteMigrations(dialect, db);
const { UserRepository } =
await import("../src/backend/database/repositories/user-repository.js");
const { HostRepository } =
await import("../src/backend/database/repositories/host-repository.js");
const { SettingsRepository } =
await import("../src/backend/database/repositories/settings-repository.js");
let failures = 0;
const check = (label, got, want) => {
const ok = JSON.stringify(got) === JSON.stringify(want);
if (!ok) failures++;
console.log(
` ${ok ? "ok " : "FAIL"} ${label}` +
(ok
? ""
: `\n got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`),
);
};
console.log(`\nverifying ${dialect} at ${url.replace(/:[^:@]*@/, ":***@")}\n`);
const users = new UserRepository(context);
const userId = `verify-${randomUUID()}`;
// insertReturning: on MySQL this is an insert plus a read inside a transaction.
const created = await users.create({
id: userId,
username: "before",
passwordHash: "x",
isAdmin: true,
});
check("insert returns the stored row", created?.username, "before");
// The one non-identity value encoder in the schema. Booleans are integers in
// the sqlite definitions the repositories import, so this asserts that 1/0
// survives a round trip through a native boolean column.
check("boolean true survives the round trip", created?.isAdmin, true);
// updateReturning must report the state AFTER the write. Reading first would
// return the value the update replaced — silently, with no error.
const updated = await users.update(userId, { username: "after" });
check("update returns the new value", updated?.username, "after");
const hosts = new HostRepository(context);
const host = await hosts.create({
userId,
name: "verify",
ip: "127.0.0.1",
port: 22,
username: "root",
authType: "password",
enableTerminal: true,
});
check(
"autoincrement id came back",
typeof host?.id === "number" && host.id > 0,
true,
);
check(
"database-assigned createdAt came back",
typeof host?.createdAt === "string" && host.createdAt.length > 0,
true,
);
// deleteReturning must report the state BEFORE the write. Reading afterwards
// would find nothing at all.
const settings = new SettingsRepository(context);
const prefix = `verify-${randomUUID()}`;
await settings.set(`${prefix}-a`, "1");
await settings.set(`${prefix}-b`, "2");
check(
"delete reports the rows it removed",
await settings.deleteLike(`${prefix}-%`),
2,
);
check(
"and they are actually gone",
(await settings.listAll()).filter((row) => row.key.startsWith(prefix)).length,
0,
);
await hosts.deleteForUser(userId, host.id);
check("host really deleted", await hosts.findById(host.id), null);
console.log(
failures === 0
? `\n${dialect}: all checks passed\n`
: `\n${dialect}: ${failures} FAILED\n`,
);
process.exit(failures === 0 ? 0 : 1);