test: isolate the reset-password CLI test and use the real schema (#461 follow-up)

The test failed in CI because another file replaces bun:sqlite with
mock.module, which leaks across files depending on order. It now runs in
an isolated child process like the other fetch and sqlite driven suites.

Each case builds its database with manage-db init, so the tables come
from drizzle/ instead of hand written DDL that could drift, and the
bundle is built once in beforeAll instead of at import time.

The Dockerfile rewrite of the manage-db scripts is now a one line sed.
This commit is contained in:
Arunavo Ray committed 2026-10-14 17:38:43 +05:30
1 parent 44d927e768
commit 89babdf461
2 files changed
+93 -52

No files matched your search

+1 -1
View File
@@ -77,7 +77,7 @@ COPY --from=builder /app/scripts/runtime-server.ts ./scripts/runtime-server.ts
COPY --from=builder /app/drizzle ./drizzle
# Database commands must use the bundled script: sources are not shipped.
RUN bun -e 'const fs = require("node:fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); for (const name of Object.keys(pkg.scripts)) { pkg.scripts[name] = pkg.scripts[name].replaceAll("bun scripts/manage-db.ts", "bun dist/scripts/manage-db.js"); } fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");'
RUN sed -i 's#bun scripts/manage-db.ts#bun dist/scripts/manage-db.js#g' package.json
# Remove build-only packages that are not needed at runtime
# (esbuild, vite, rollup, tailwind, svgo — all only used during `astro build`)
+92 -51
View File
@@ -1,14 +1,44 @@
import { afterAll, expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
/**
* Regression test for the password reset CLI (#461).
*
* `manage-db reset-password` used a Drizzle client without a schema, so
* `db.query.users` was undefined and every reset failed. Each case builds a
* real database with `manage-db init` (the migrations in drizzle/), seeds it,
* runs the reset through the source script and through the bundle the Docker
* image ships, and checks the result with Better Auth.
*
* Other test files replace bun:sqlite with mock.module, which is process wide
* in bun and depends on file order, so the suite runs in an isolated child
* process (same harness as gitea-incremental-metadata.test.ts).
*/
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { verifyPassword } from "better-auth/crypto";
const CHILD_FLAG = "GM_RESET_PASSWORD_CLI_ISOLATED";
const isChild = !!process.env[CHILD_FLAG];
if (!isChild) {
test("reset-password CLI - isolated child suite", () => {
const res = Bun.spawnSync({
cmd: [process.execPath, "test", import.meta.path],
env: { ...process.env, [CHILD_FLAG]: "1" },
stdout: "pipe",
stderr: "pipe",
});
if (res.exitCode !== 0) {
console.error(res.stdout.toString());
console.error(res.stderr.toString());
}
expect(res.exitCode).toBe(0);
}, 60_000);
}
const root = resolve(import.meta.dir, "../../..");
const temporaryRoot = mkdtempSync(join(tmpdir(), "gitea-mirror-reset-password-"));
const bundle = join(temporaryRoot, "manage-db.js");
afterAll(() => rmSync(temporaryRoot, { recursive: true, force: true }));
let temporaryRoot = "";
let bundle = "";
function run(cmd: string[], cwd: string) {
const result = Bun.spawnSync({ cmd, cwd, stdout: "pipe", stderr: "pipe" });
@@ -16,50 +46,61 @@ function run(cmd: string[], cwd: string) {
return Buffer.from(result.stdout).toString();
}
run([process.execPath, "build", "scripts/manage-db.ts", "--target=bun", `--outfile=${bundle}`], root);
describe.skipIf(!isChild)("manage-db reset-password", () => {
beforeAll(() => {
temporaryRoot = mkdtempSync(join(tmpdir(), "gitea-mirror-reset-password-"));
bundle = join(temporaryRoot, "manage-db.js");
run([process.execPath, "build", "scripts/manage-db.ts", "--target=bun", `--outfile=${bundle}`], root);
});
afterAll(() => {
if (temporaryRoot) rmSync(temporaryRoot, { recursive: true, force: true });
});
for (const bundled of [false, true]) {
for (const existingCredential of [false, true]) {
test(`reset-password (${bundled ? "bundled" : "source"}, ${existingCredential ? "existing" : "new"} credential)`, async () => {
const cwd = mkdtempSync(join(temporaryRoot, "case-"));
mkdirSync(join(cwd, "data"));
const db = new Database(join(cwd, "data/gitea-mirror.db"));
try {
db.exec(`
CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT, email TEXT NOT NULL UNIQUE,
email_verified INTEGER DEFAULT 0, image TEXT, created_at INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0, username TEXT);
CREATE TABLE accounts (id TEXT PRIMARY KEY, account_id TEXT NOT NULL,
user_id TEXT NOT NULL, provider_id TEXT NOT NULL, issuer TEXT DEFAULT '',
provider_user_id TEXT, access_token TEXT, refresh_token TEXT, id_token TEXT,
access_token_expires_at INTEGER, refresh_token_expires_at INTEGER, scope TEXT,
expires_at INTEGER, password TEXT, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0);
CREATE TABLE sessions (id TEXT PRIMARY KEY, token TEXT NOT NULL, user_id TEXT NOT NULL,
expires_at INTEGER, ip_address TEXT, user_agent TEXT, created_at INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0);
INSERT INTO users (id, email) VALUES ('target', 'admin@example.com'), ('other', 'other@example.com');
INSERT INTO accounts (id, account_id, user_id, provider_id, password)
VALUES ('oauth', 'remote', 'target', 'github', NULL), ('other-account', 'other', 'other', 'credential', 'unchanged');
INSERT INTO sessions (id, token, user_id) VALUES ('one', 'one', 'target'), ('two', 'two', 'target'), ('other', 'other', 'other');
`);
if (existingCredential) {
db.exec("INSERT INTO accounts (id, account_id, user_id, provider_id, password) VALUES ('credential', 'target', 'target', 'credential', 'old')");
for (const bundled of [false, true]) {
for (const existingCredential of [false, true]) {
test(`${bundled ? "bundled" : "source"} script, ${existingCredential ? "existing" : "new"} credential`, async () => {
const { Database } = await import("bun:sqlite");
const { verifyPassword } = await import("better-auth/crypto");
const script = bundled ? bundle : join(root, "scripts/manage-db.ts");
// A real schema: init runs the migrations from ./drizzle.
const cwd = mkdtempSync(join(temporaryRoot, "case-"));
symlinkSync(join(root, "drizzle"), join(cwd, "drizzle"));
run([process.execPath, script, "init"], cwd);
const db = new Database(join(cwd, "data/gitea-mirror.db"));
try {
db.exec(`
INSERT INTO users (id, name, email) VALUES ('target', 'Target', 'admin@example.com'), ('other', 'Other', 'other@example.com');
INSERT INTO accounts (id, account_id, user_id, provider_id, password)
VALUES ('oauth', 'remote', 'target', 'github', NULL), ('other-account', 'other', 'other', 'credential', 'unchanged');
INSERT INTO sessions (id, token, user_id, expires_at) VALUES ('one', 'one', 'target', 0), ('two', 'two', 'target', 0), ('other', 'other', 'other', 0);
`);
if (existingCredential) {
db.exec("INSERT INTO accounts (id, account_id, user_id, provider_id, password) VALUES ('credential', 'target', 'target', 'credential', 'old')");
}
const output = run(
[process.execPath, script, "reset-password", "--email=admin@example.com", "--new-password=new-secure-password"],
cwd
);
expect(output).toContain("Password reset for admin@example.com");
expect(output).toContain("Cleared 2 active session(s)");
const credentials = db
.query("SELECT * FROM accounts WHERE user_id = 'target' AND provider_id = 'credential'")
.all() as Array<{ password: string; issuer: string }>;
expect(credentials).toHaveLength(1);
expect(await verifyPassword({ hash: credentials[0].password, password: "new-secure-password" })).toBe(true);
if (!existingCredential) expect(credentials[0].issuer).toBe("local:credential");
expect(db.query("SELECT id FROM sessions").all()).toEqual([{ id: "other" }]);
expect(db.query("SELECT password FROM accounts WHERE id = 'other-account'").get()).toEqual({ password: "unchanged" });
expect(db.query("SELECT password FROM accounts WHERE id = 'oauth'").get()).toEqual({ password: null });
expect(db.query("SELECT id FROM users").all()).toHaveLength(2);
} finally {
db.close();
}
const output = run([process.execPath, bundled ? bundle : join(root, "scripts/manage-db.ts"),
"reset-password", "--email=admin@example.com", "--new-password=new-secure-password"], cwd);
expect(output).toContain("Password reset for admin@example.com");
expect(output).toContain("Cleared 2 active session(s)");
const credentials = db.query("SELECT * FROM accounts WHERE user_id = 'target' AND provider_id = 'credential'").all() as Array<{ password: string; issuer: string }>;
expect(credentials).toHaveLength(1);
expect(await verifyPassword({ hash: credentials[0].password, password: "new-secure-password" })).toBe(true);
if (!existingCredential) expect(credentials[0].issuer).toBe("local:credential");
expect(db.query("SELECT id FROM sessions").all()).toEqual([{ id: "other" }]);
expect(db.query("SELECT password FROM accounts WHERE id = 'other-account'").get()).toEqual({ password: "unchanged" });
expect(db.query("SELECT password FROM accounts WHERE id = 'oauth'").get()).toEqual({ password: null });
expect(db.query("SELECT id FROM users").all()).toHaveLength(2);
} finally {
db.close();
}
}, 15000);
}, 15_000);
}
}
}
});