Bring the tests back in line with the code (#82)
* Mark the fixture admin as primary admin in the account tests
Creating accounts needs a primary admin or an admin with user management rights since migration 0050, but the fixture inserted its admin without is_primary_admin, so every create call got a 403.
* Let the agent tool test call mutating tools without chat approval
In chat, mark_email_read, move_email and discard_draft now return pending_approval instead of acting. The test still expected the change to happen right away. It now checks that chat only asks, and runs the direct calls with the mcp origin.
* Test the migrated schema instead of the removed bootstrap SQL
The old test read MIGRATION_NAMES and INITIAL_SCHEMA_SQL out of src/lib/setup/migration.ts, which now just runs the migration files. The replacement applies drizzle/migrations to an empty database, checks that every file is recorded and not re-applied, and keeps the mailbox, license and spam table checks.
* Expect the shortened pt-BR inbox label in the i18n tests
6978c01 changed navigation.inbox from "Caixa de entrada" to "Entrada" in pt-BR, but four tests still expected the old label.
---------
Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
This commit is contained in:
1 parent
8ee28cfd04
commit
21817cedcb
4 files changed
+71
-78
No files matched your search
@@ -48,9 +48,9 @@ async function fixture(t, { rules = [], failAddress, slowAddress } = {}) {
|
||||
t.after(() => database.db.close());
|
||||
await applyMigrations(database, join(root, "drizzle/migrations"));
|
||||
database.db.exec(`
|
||||
INSERT INTO users (id, email, password_hash, name, role, created_at) VALUES
|
||||
('admin', 'owner@one.test', 'hash', 'Owner', 'admin', 1),
|
||||
('other', 'owner@foreign.test', 'hash', 'Other', 'admin', 1);
|
||||
INSERT INTO users (id, email, password_hash, name, role, is_primary_admin, created_at) VALUES
|
||||
('admin', 'owner@one.test', 'hash', 'Owner', 'admin', 1, 1),
|
||||
('other', 'owner@foreign.test', 'hash', 'Other', 'admin', 0, 1);
|
||||
INSERT INTO domains (id, user_id, hostname, zone_id, status, created_at) VALUES
|
||||
('one', 'admin', 'one.test', 'zone-one', 'active', 1),
|
||||
('two', 'admin', 'two.test', 'zone-two', 'active', 1),
|
||||
|
||||
@@ -76,6 +76,7 @@ test("assistant reads mail, creates an editable reply draft, and invokes tools t
|
||||
const env = { DB: database, BUCKET: { get: async () => null, delete: async () => {} }, AI_BASE_URL: `http://127.0.0.1:${address.port}/v1`, AI_API_KEY: "test-key", AI_MODEL: "test-model" };
|
||||
const context = { env, user: { id: "user-1", email: "owner@example.com", role: "user" }, mailboxId: "mailbox-1", origin: "chat" };
|
||||
|
||||
const direct = { ...context, origin: "mcp" };
|
||||
const listed = await runEmailTool(context, "list_emails", { folder: "inbox", limit: 20 });
|
||||
assert.equal(listed.emails[0].id, "email-1");
|
||||
const email = await runEmailTool(context, "get_email", { emailId: "email-1" });
|
||||
@@ -94,13 +95,16 @@ test("assistant reads mail, creates an editable reply draft, and invokes tools t
|
||||
assert.equal(database.db.prepare("SELECT count(*) AS count FROM agent_send_approvals").get().count, 0);
|
||||
const newDraft = await runEmailTool(context, "draft_email", { to: "customer@example.net", subject: "Follow up", body: "Hello again." });
|
||||
assert.equal(database.db.prepare("SELECT status FROM messages WHERE id = ?").get(newDraft.draftId).status, "draft");
|
||||
await runEmailTool(context, "discard_draft", { draftId: newDraft.draftId, expectedRevision: 1 });
|
||||
const pendingDiscard = await runEmailTool(context, "discard_draft", { draftId: newDraft.draftId, expectedRevision: 1 });
|
||||
assert.equal(pendingDiscard.status, "pending_approval");
|
||||
assert.ok(database.db.prepare("SELECT id FROM messages WHERE id = ?").get(newDraft.draftId));
|
||||
await runEmailTool(direct, "discard_draft", { draftId: newDraft.draftId, expectedRevision: 1 });
|
||||
assert.equal(database.db.prepare("SELECT id FROM messages WHERE id = ?").get(newDraft.draftId), undefined);
|
||||
await runEmailTool(context, "mark_email_read", { emailId: "email-1", read: true });
|
||||
await runEmailTool(direct, "mark_email_read", { emailId: "email-1", read: true });
|
||||
assert.equal(database.db.prepare("SELECT read FROM messages WHERE id = 'email-1'").get().read, 1);
|
||||
await runEmailTool(context, "move_email", { emailId: "email-1", destination: "archived" });
|
||||
await runEmailTool(direct, "move_email", { emailId: "email-1", destination: "archived" });
|
||||
assert.equal(database.db.prepare("SELECT status FROM messages WHERE id = 'email-1'").get().status, "archived");
|
||||
await runEmailTool(context, "move_email", { emailId: "email-1", destination: "inbox" });
|
||||
await runEmailTool(direct, "move_email", { emailId: "email-1", destination: "inbox" });
|
||||
|
||||
const { stream } = await createAgentChatStream(context, "What arrived in my inbox?");
|
||||
const events = (await new Response(stream).text()).trim().split("\n").map((line) => JSON.parse(line));
|
||||
|
||||
+4
-4
@@ -120,7 +120,7 @@ test("missing, invalid and unsupported locale values fall back to English", () =
|
||||
});
|
||||
|
||||
test("translation uses the requested catalog and falls back per key", async () => {
|
||||
assert.equal(i18n.translate(await i18n.loadMessages("pt-BR"), "navigation.inbox"), "Caixa de entrada");
|
||||
assert.equal(i18n.translate(await i18n.loadMessages("pt-BR"), "navigation.inbox"), "Entrada");
|
||||
assert.equal(i18n.translate({}, "navigation.inbox"), "Inbox");
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ test("preference cookie persists for a year, covers every path and is Secure on
|
||||
|
||||
test("provider server rendering uses initial locale and labels the native language selector", async () => {
|
||||
const pt = await i18n.renderLanguage("pt-BR");
|
||||
assert.match(pt, /Caixa de entrada/);
|
||||
assert.match(pt, /<span>Entrada<\/span>/);
|
||||
assert.match(pt, /<label for="[^"]+"[^>]*>Idioma<\/label>/);
|
||||
assert.match(pt, /<select id="[^"]+"/);
|
||||
assert.match(pt, /value="pt-BR" lang="pt-BR" selected=""/);
|
||||
@@ -142,7 +142,7 @@ test("provider server rendering uses initial locale and labels the native langua
|
||||
test("root layout passes the same cookie locale to HTML and provider on first render", async () => {
|
||||
const pt = await i18n.renderLayout("pt-BR");
|
||||
assert.match(pt, /<html lang="pt-BR"/);
|
||||
assert.match(pt, /Caixa de entrada/);
|
||||
assert.match(pt, /<span>Entrada<\/span>/);
|
||||
for (const value of [undefined, "bad"]) {
|
||||
const english = await i18n.renderLayout(value);
|
||||
assert.match(english, /<html lang="en"/);
|
||||
@@ -178,6 +178,6 @@ test("a locale registered once reaches resolution, translation, cookie, selector
|
||||
test("translations interpolate {vars}, keep unknown placeholders and default to ltr", async () => {
|
||||
assert.equal(i18n.translate({ "navigation.inbox": "Hi {name}, {other}" }, "navigation.inbox", { name: "Ana", count: 2 }), "Hi Ana, {other}");
|
||||
assert.equal(i18n.translate({}, "navigation.inbox", { name: "x" }), "Inbox");
|
||||
assert.equal(i18n.createTranslator("pt-BR", await i18n.loadMessages("pt-BR"))("navigation.inbox", { count: 2 }), "Caixa de entrada");
|
||||
assert.equal(i18n.createTranslator("pt-BR", await i18n.loadMessages("pt-BR"))("navigation.inbox", { count: 2 }), "Entrada");
|
||||
assert.equal(i18n.getDirection("en"), "ltr");
|
||||
});
|
||||
@@ -1,77 +1,66 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import test, { after } from "node:test";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const src = readFileSync(join(root, "src/lib/setup/migration.ts"), "utf8");
|
||||
const migrationsDirectory = join(root, "drizzle/migrations");
|
||||
const bundleDirectory = mkdtempSync(join(root, "node_modules", "mailflare-bootstrap-bundle-"));
|
||||
after(() => rmSync(bundleDirectory, { recursive: true, force: true }));
|
||||
|
||||
function migrationNames() {
|
||||
const match = src.match(/const MIGRATION_NAMES = \[([\s\S]*?)\];/);
|
||||
assert.ok(match, "MIGRATION_NAMES array not found");
|
||||
return [...match[1].matchAll(/"([^"]+)"/g)].map((item) => item[1]);
|
||||
}
|
||||
await build({
|
||||
stdin: {
|
||||
contents: `
|
||||
export { SqliteDatabase } from "./server/runtime/sqlite-database.ts";
|
||||
export { applyMigrations } from "./server/runtime/migrate.ts";
|
||||
`,
|
||||
resolveDir: root,
|
||||
sourcefile: "bootstrap-schema-test-entry.ts",
|
||||
},
|
||||
outfile: join(bundleDirectory, "entry.mjs"),
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
format: "esm",
|
||||
target: "node24",
|
||||
tsconfig: join(root, "tsconfig.json"),
|
||||
packages: "external",
|
||||
logLevel: "silent",
|
||||
});
|
||||
const { SqliteDatabase, applyMigrations } = await import(pathToFileURL(join(bundleDirectory, "entry.mjs")).href);
|
||||
|
||||
function initialSchemaSql() {
|
||||
const match = src.match(/const INITIAL_SCHEMA_SQL = `([\s\S]*?)`;/);
|
||||
assert.ok(match, "INITIAL_SCHEMA_SQL not found");
|
||||
return match[1];
|
||||
}
|
||||
const columns = (database, table) => database.db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name);
|
||||
|
||||
test("bootstrap records migrations represented in the current schema so later deploys do not re-apply them", () => {
|
||||
const names = migrationNames();
|
||||
for (const name of [
|
||||
"0013_add_license_settings.sql",
|
||||
"0021_add_mailbox_signature.sql",
|
||||
"0022_add_mailbox_auto_reply.sql",
|
||||
"0027_add_domain_sending_intent.sql",
|
||||
"0029_add_spam_protection.sql",
|
||||
]) {
|
||||
assert.ok(names.includes(name), `MIGRATION_NAMES is missing ${name}`);
|
||||
test("a fresh database records every migration, so later deploys do not re-apply them", async (t) => {
|
||||
const database = new SqliteDatabase(":memory:");
|
||||
t.after(() => database.db.close());
|
||||
const files = readdirSync(migrationsDirectory).filter((name) => name.endsWith(".sql"));
|
||||
const applied = await applyMigrations(database, migrationsDirectory);
|
||||
assert.deepEqual([...applied].sort(), [...files].sort());
|
||||
const recorded = database.db.prepare("SELECT name FROM d1_migrations").all().map((row) => row.name);
|
||||
assert.deepEqual(recorded.sort(), [...files].sort());
|
||||
assert.deepEqual(await applyMigrations(database, migrationsDirectory), []);
|
||||
});
|
||||
|
||||
test("fresh schema accepts the current Drizzle mailbox and license inserts", async (t) => {
|
||||
const database = new SqliteDatabase(":memory:");
|
||||
t.after(() => database.db.close());
|
||||
await applyMigrations(database, migrationsDirectory);
|
||||
const db = database.db;
|
||||
|
||||
assert.ok(["signature", "auto_reply_enabled", "auto_reply_subject", "auto_reply_body"].every((name) => columns(database, "mailboxes").includes(name)));
|
||||
assert.ok(columns(database, "domains").includes("sending_requested"));
|
||||
for (const table of ["license_settings", "auto_reply_deliveries", "spam_token_stats", "spam_reputation", "spam_feedback"]) {
|
||||
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table), `${table} is missing`);
|
||||
}
|
||||
});
|
||||
|
||||
test("fresh bootstrap schema accepts the current Drizzle mailbox and license inserts", () => {
|
||||
const sql = initialSchemaSql();
|
||||
const mailboxCreate = sql.match(/CREATE TABLE IF NOT EXISTS mailboxes \(([\s\S]*?)\);/);
|
||||
assert.ok(mailboxCreate, "mailboxes CREATE TABLE not found");
|
||||
assert.match(mailboxCreate[1], /\bsignature\b/);
|
||||
assert.match(mailboxCreate[1], /\bauto_reply_enabled\b/);
|
||||
assert.match(mailboxCreate[1], /\bauto_reply_subject\b/);
|
||||
assert.match(mailboxCreate[1], /\bauto_reply_body\b/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS license_settings \(/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS auto_reply_deliveries \(/);
|
||||
assert.match(sql, /\bspam_protection_enabled\b/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS spam_token_stats \(/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS spam_reputation \(/);
|
||||
assert.match(sql, /CREATE TABLE IF NOT EXISTS spam_feedback \(/);
|
||||
const domainCreate = sql.match(/CREATE TABLE IF NOT EXISTS domains \(([\s\S]*?)\);/);
|
||||
assert.ok(domainCreate, "domains CREATE TABLE not found");
|
||||
assert.match(domainCreate[1], /\bsending_requested\b/);
|
||||
|
||||
const py = `
|
||||
import sqlite3, sys
|
||||
sql = sys.stdin.read()
|
||||
db = sqlite3.connect(":memory:")
|
||||
for stmt in sql.split(";"):
|
||||
s = stmt.strip()
|
||||
if s:
|
||||
db.execute(s)
|
||||
db.execute("INSERT INTO users (id,email,password_hash,name,created_at) VALUES ('u','a@b.c','x','n',1)")
|
||||
db.execute("INSERT INTO domains (id,user_id,hostname,zone_id,created_at) VALUES ('d','u','ex.com','z',1)")
|
||||
db.execute("""
|
||||
INSERT INTO mailboxes (
|
||||
id, user_id, domain_id, local_part, display_name,
|
||||
signature, auto_reply_enabled, auto_reply_subject, auto_reply_body, created_at
|
||||
) VALUES ('m','u','d','admin','admin','sig',0,'Out of office','',1)
|
||||
""")
|
||||
db.execute("INSERT INTO license_settings (id, instance_id, updated_at) VALUES ('default','inst',1)")
|
||||
db.execute("INSERT INTO auto_reply_deliveries (id, mailbox_id, recipient, sent_at) VALUES ('ar','m','x@y.z',1)")
|
||||
print("ok")
|
||||
`;
|
||||
const result = spawnSync("python3", ["-c", py], { input: sql, encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr + result.stdout);
|
||||
assert.match(result.stdout, /^ok$/m);
|
||||
db.prepare("INSERT INTO users (id, email, password_hash, name, created_at) VALUES ('u', 'a@b.c', 'x', 'n', 1)").run();
|
||||
db.prepare("INSERT INTO domains (id, user_id, hostname, zone_id, created_at) VALUES ('d', 'u', 'ex.com', 'z', 1)").run();
|
||||
db.prepare(`
|
||||
INSERT INTO mailboxes (id, user_id, domain_id, local_part, display_name, signature, auto_reply_enabled, auto_reply_subject, auto_reply_body, created_at)
|
||||
VALUES ('m', 'u', 'd', 'admin', 'admin', 'sig', 0, 'Out of office', '', 1)
|
||||
`).run();
|
||||
db.prepare("INSERT INTO license_settings (id, instance_id, updated_at) VALUES ('default', 'inst', 1)").run();
|
||||
assert.equal(db.prepare("SELECT count(*) AS count FROM mailboxes").get().count, 1);
|
||||
});
|
||||
Reference in new issue
Block a user