Files
mailflare/tests/account-alias-selection.test.mjs
Can K.andCan Kirca 21817cedcb 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>
2026-10-11 02:06:49 +07:00

289 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
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 directory = mkdtempSync(join(root, "node_modules", "mailflare-account-test-"));
after(() => rmSync(directory, { recursive: true, force: true }));
await build({
stdin: {
contents: `
export { SqliteDatabase } from "./server/runtime/sqlite-database.ts";
export { applyMigrations } from "./server/runtime/migrate.ts";
export { getDb } from "./src/db/index.ts";
export { createSession } from "./src/lib/auth/session.ts";
export { generateApiKey } from "./src/lib/api-keys.ts";
export { createUserAccountSchema } from "./src/lib/validators.ts";
export { POST as dashboardCreate } from "./src/app/api/accounts/route.ts";
export { POST as apiCreate } from "./src/app/api/v1/accounts/route.ts";
export { registerAdminMcpTools } from "./src/lib/mcp/admin-tools.ts";
export { getMailboxDomainAddresses } from "./src/lib/mailboxes/domain-addresses.ts";
export { resolveInboundAddress } from "./src/lib/email/routing.ts";
export { getAuthorizedSenderAddress } from "./src/lib/email/sender.ts";
`,
resolveDir: root,
sourcefile: "account-alias-test-entry.ts",
},
outfile: join(directory, "entry.mjs"),
bundle: true,
platform: "node",
format: "esm",
target: "node24",
tsconfig: join(root, "tsconfig.json"),
packages: "external",
alias: { "next/headers": "next/headers.js", "next/server": "next/server.js", "cloudflare:workers": "./server/runtime/cloudflare-workers.ts" },
logLevel: "silent",
});
const {
SqliteDatabase, applyMigrations, getDb, createSession, generateApiKey, createUserAccountSchema,
dashboardCreate, apiCreate, registerAdminMcpTools, getMailboxDomainAddresses, resolveInboundAddress, getAuthorizedSenderAddress,
} = await import(pathToFileURL(join(directory, "entry.mjs")).href);
const key = generateApiKey();
async function fixture(t, { rules = [], failAddress, slowAddress } = {}) {
const database = new SqliteDatabase(":memory:");
t.after(() => database.db.close());
await applyMigrations(database, join(root, "drizzle/migrations"));
database.db.exec(`
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),
('pending', 'admin', 'pending.test', 'zone-pending', 'pending', 1),
('foreign', 'other', 'foreign.test', 'zone-foreign', 'active', 1);
INSERT INTO license_settings (id, instance_id, plan, state, features, updated_at)
VALUES ('default', 'test-instance', 'team', 'active', '[]', 1);
`);
database.db.prepare("INSERT INTO api_keys (id, user_id, name, prefix, key_hash, scopes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
.run("key", "admin", "Test", key.prefix, key.hash, '["accounts"]', 1);
const env = { DB: database, CF_TOKEN: "test-only", MAILFLARE_RUNTIME: "node" };
globalThis.__mailflareNodeEnv = env;
t.after(() => { delete globalThis.__mailflareNodeEnv; });
const session = await createSession(env, "admin");
const currentRules = structuredClone(rules);
const calls = [];
let nextRule = 0;
t.mock.method(globalThis, "fetch", async (url, init = {}) => {
const parsed = new URL(url);
assert.equal(parsed.origin, "https://api.cloudflare.com", "tests must never contact a real service");
const match = parsed.pathname.match(/^\/client\/v4\/zones\/([^/]+)\/email\/routing\/rules(?:\/([^/]+))?$/);
assert.ok(match, parsed.pathname);
const [, zoneId, ruleId] = match;
const method = init.method ?? "GET";
const body = init.body ? JSON.parse(init.body) : null;
calls.push({ method, zoneId, ruleId, body });
if (method === "GET") return Response.json({ success: true, result: structuredClone(currentRules.filter((rule) => rule.zoneId === zoneId)) });
const address = body?.matchers?.[0]?.value;
if (slowAddress && address === slowAddress) await new Promise((resolve) => setTimeout(resolve, 20));
if (failAddress && address === failAddress) return Response.json({ success: false, errors: [{ message: "Simulated routing failure" }] }, { status: 502 });
if (method === "POST") {
const rule = { ...body, id: `new-${++nextRule}`, zoneId };
currentRules.push(rule);
return Response.json({ success: true, result: rule });
}
const index = currentRules.findIndex((rule) => rule.id === ruleId && rule.zoneId === zoneId);
assert.notEqual(index, -1);
if (method === "DELETE") currentRules.splice(index, 1);
if (method === "PUT") currentRules[index] = { ...body, id: ruleId, zoneId };
return Response.json({ success: true, result: currentRules[index] ?? null });
});
const post = (kind, overrides = {}, token) => {
const handler = kind === "dashboard" ? dashboardCreate : apiCreate;
return handler(new Request(`https://mailflare.test/api/${kind === "dashboard" ? "" : "v1/"}accounts`, {
method: "POST",
headers: { Authorization: `Bearer ${token ?? (kind === "dashboard" ? session : key.fullKey)}`, "Content-Type": "application/json" },
body: JSON.stringify({ username: "sam", domainId: "one", password: "test-password", ...overrides }),
}));
};
const mailbox = () => {
const row = database.db.prepare("SELECT * FROM mailboxes WHERE local_part = 'sam'").get();
return row && { id: row.id, userId: row.user_id, domainId: row.domain_id, localPart: row.local_part, useAllDomains: !!row.use_all_domains };
};
return { database, env, db: getDb(env), post, mailbox, calls, currentRules };
}
for (const kind of ["dashboard", "api"]) {
test(`${kind}: primary-only creation does not grant addresses on other domains`, async (t) => {
const f = await fixture(t);
assert.equal((await f.post(kind, { useAllDomains: false })).status, 201);
const mailbox = f.mailbox();
assert.ok(f.database.db.prepare("SELECT booking_username FROM users WHERE id = ?").get(mailbox.userId).booking_username);
assert.equal(mailbox.useAllDomains, false);
assert.deepEqual(await getMailboxDomainAddresses(f.db, mailbox), ["sam@one.test"]);
assert.equal((await resolveInboundAddress(f.db, "sam@one.test")).mailbox.mailboxId, mailbox.id);
assert.equal(await resolveInboundAddress(f.db, "sam@two.test"), null);
await assert.rejects(getAuthorizedSenderAddress(f.env, { userId: mailbox.userId, mailboxId: mailbox.id, from: "sam@two.test" }), /Sender address/);
assert.deepEqual(f.calls.filter((call) => call.method === "POST").map((call) => call.body.matchers[0].value), ["sam@one.test"]);
});
test(`${kind}: explicit aliases receive and send through the same mailbox`, async (t) => {
const f = await fixture(t);
const response = await f.post(kind, { useAllDomains: false, aliases: [{ domainId: "two", localPart: " Sales " }, { domainId: "one", localPart: "support" }] });
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.account.email, "sam@one.test");
assert.equal("passwordHash" in body.account, false);
const mailbox = f.mailbox();
assert.deepEqual((await getMailboxDomainAddresses(f.db, mailbox)).sort(), ["sales@two.test", "sam@one.test", "support@one.test"]);
assert.equal((await resolveInboundAddress(f.db, "sales@two.test")).mailbox.mailboxId, mailbox.id);
assert.equal((await getAuthorizedSenderAddress(f.env, { userId: mailbox.userId, mailboxId: mailbox.id, from: "sales@two.test" })).mailboxId, mailbox.id);
assert.equal(await resolveInboundAddress(f.db, "sam@two.test"), null);
f.database.db.exec("INSERT INTO domains (id, user_id, hostname, zone_id, status, created_at) VALUES ('later', 'admin', 'later.test', 'manual', 'active', 1)");
assert.equal(await resolveInboundAddress(f.db, "sam@later.test"), null);
assert.equal((await getMailboxDomainAddresses(f.db, mailbox)).includes("sam@later.test"), false);
});
}
test("omitted settings preserve API compatibility; opting in includes future domains", async (t) => {
const f = await fixture(t);
assert.equal((await f.post("api")).status, 201);
assert.equal(f.mailbox().useAllDomains, true);
assert.deepEqual(await getMailboxDomainAddresses(f.db, f.mailbox()), ["sam@one.test", "sam@two.test"]);
f.database.db.exec("INSERT INTO domains (id, user_id, hostname, zone_id, status, created_at) VALUES ('later', 'admin', 'later.test', 'manual', 'active', 1)");
assert.equal((await resolveInboundAddress(f.db, "sam@later.test")).mailbox.mailboxId, f.mailbox().id);
assert.equal((await f.post("dashboard", { username: "jane", useAllDomains: true, aliases: [{ domainId: "two", localPart: "billing" }] })).status, 201);
const address = await resolveInboundAddress(f.db, "billing@two.test");
assert.equal(address.mailbox.localPart, "jane");
});
test("admin MCP forwards alias selection to the same account creation handler", async (t) => {
const f = await fixture(t);
const handlers = new Map();
registerAdminMcpTools({ registerTool: (name, _schema, handler) => handlers.set(name, handler) }, { scopes: ["accounts"] }, "https://mailflare.test", `Bearer ${key.fullKey}`);
const result = await handlers.get("manage_accounts")({ action: "create", data: { username: "sam", domainId: "one", password: "test-password", useAllDomains: false, aliases: [{ domainId: "two", localPart: "sales" }] } });
assert.equal(result.isError, false);
assert.equal(JSON.parse(result.content[0].text).status, 201);
assert.deepEqual((await getMailboxDomainAddresses(f.db, f.mailbox())).sort(), ["sales@two.test", "sam@one.test"]);
});
test("invalid, duplicate, foreign, and inactive aliases fail before provisioning", async (t) => {
const f = await fixture(t);
for (const aliases of [
[{ domainId: "one", localPart: "s.a.m+tag" }],
[{ domainId: "two", localPart: "sales" }, { domainId: "two", localPart: "SALES" }],
[{ domainId: "foreign", localPart: "sales" }],
[{ domainId: "pending", localPart: "sales" }],
[{ domainId: "missing", localPart: "sales" }],
[{ domainId: "two", localPart: "not an address" }],
[{ domainId: "two", localPart: "+tag" }],
]) {
assert.equal((await f.post("api", { useAllDomains: false, aliases })).status, 400, JSON.stringify(aliases));
}
assert.equal(f.calls.length, 0);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM users").get().count, 2);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailbox_aliases").get().count, 0);
});
test("mailbox and alias conflicts include normalized primary addresses", async (t) => {
const f = await fixture(t);
f.database.db.exec(`
INSERT INTO mailboxes (id, user_id, domain_id, local_part, created_at) VALUES ('existing', 'admin', 'two', 'sales', 1);
INSERT INTO mailbox_aliases (id, mailbox_id, domain_id, local_part, created_at) VALUES ('alias', 'existing', 'one', 'support', 1);
`);
for (const input of [
{ aliases: [{ domainId: "two", localPart: "s.a.l.e.s+tag" }] },
{ aliases: [{ domainId: "one", localPart: "SUPPORT" }] },
{ username: "support" },
{ username: "s.u.p.p.o.r.t+tag" },
]) assert.equal((await f.post("dashboard", { useAllDomains: false, ...input })).status, 409);
assert.equal(f.calls.length, 0);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM users").get().count, 2);
});
test("explicit addresses take precedence over implicit all-domain aliases", async (t) => {
const f = await fixture(t);
f.database.db.exec("INSERT INTO mailboxes (id, user_id, domain_id, local_part, use_all_domains, created_at) VALUES ('existing', 'admin', 'one', 'sales', 1, 1)");
assert.equal((await f.post("api", { useAllDomains: false, aliases: [{ domainId: "two", localPart: "sales" }] })).status, 201);
assert.equal((await resolveInboundAddress(f.db, "sales@two.test")).mailbox.mailboxId, f.mailbox().id);
assert.equal((await resolveInboundAddress(f.db, "sales@one.test")).mailbox.mailboxId, "existing");
});
test("database failures leave no partial account, aliases, or routing rules", async (t) => {
const f = await fixture(t);
f.database.db.exec("CREATE TRIGGER fail_alias BEFORE INSERT ON mailbox_aliases WHEN NEW.local_part = 'alias40' BEGIN SELECT RAISE(ABORT, 'Simulated insert failure'); END");
const aliases = Array.from({ length: 41 }, (_, i) => ({ domainId: "two", localPart: `alias${i}` }));
assert.equal((await f.post("api", { useAllDomains: false, aliases })).status, 502);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM users").get().count, 2);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailboxes").get().count, 0);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailbox_aliases").get().count, 0);
assert.equal(f.calls.length, 0);
});
function workerRule(id, address, enabled) {
return { id, zoneId: "zone-one", enabled, name: "Existing route", priority: 20, actions: [{ type: "worker", value: ["mailflare"] }], matchers: [{ type: "literal", field: "to", value: address }] };
}
for (const enabled of [true, false]) {
test(`routing failure cleans up completed requests and preserves a previously ${enabled ? "enabled" : "disabled"} rule`, async (t) => {
const original = workerRule("old", "sam@one.test", enabled);
const f = await fixture(t, { rules: [original], failAddress: "broken@two.test", slowAddress: "slow@two.test" });
const response = await f.post("api", { useAllDomains: false, aliases: [{ domainId: "two", localPart: "slow" }, { domainId: "two", localPart: "broken" }] });
assert.equal(response.status, 502);
assert.deepEqual(f.currentRules, [original]);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM users").get().count, 2);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailboxes").get().count, 0);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailbox_aliases").get().count, 0);
assert.ok(f.calls.some((call) => call.method === "DELETE"), "a successfully provisioned alias must be rolled back");
});
}
test("account creation still enforces admin authorization and the Team license", async (t) => {
const f = await fixture(t);
assert.equal((await f.post("api", {}, "invalid-test-key")).status, 401);
assert.equal((await f.post("dashboard", {}, "invalid-test-session")).status, 403);
f.database.db.exec("UPDATE license_settings SET state = 'inactive'");
assert.equal((await f.post("api")).status, 403);
assert.equal((await f.post("dashboard")).status, 403);
assert.equal(f.calls.length, 0);
});
test("a Pro license creates accounts only while seats remain; Team and unlimited Pro have no limit", async (t) => {
const f = await fixture(t);
const users = () => f.database.db.prepare("SELECT count(*) AS count FROM users WHERE disabled = 0").get().count;
f.database.db.exec(`UPDATE license_settings SET plan = 'pro', seat_limit = ${users()}`);
assert.equal((await f.post("api")).status, 403, "no seat left");
f.database.db.exec(`UPDATE license_settings SET seat_limit = ${users() + 1}`);
assert.equal((await f.post("api", { username: "seat-one" })).status, 201);
assert.equal((await f.post("api", { username: "seat-two" })).status, 403, "seat just used");
f.database.db.exec("UPDATE license_settings SET seat_limit = NULL");
assert.equal((await f.post("api", { username: "seat-three" })).status, 201);
f.database.db.exec("UPDATE license_settings SET plan = 'team', seat_limit = 1");
assert.equal((await f.post("api", { username: "seat-four" })).status, 201);
});
for (const separateDomains of [false, true]) {
test(`more than 50 aliases on ${separateDomains ? "separate domains" : "one domain"} stay within database parameter limits`, async (t) => {
const f = await fixture(t);
f.database.db.exec("UPDATE domains SET zone_id = 'manual'");
const aliases = Array.from({ length: 125 }, (_, i) => ({ domainId: separateDomains ? `domain${i}` : "two", localPart: `alias${i}` }));
if (separateDomains) {
const insert = f.database.db.prepare("INSERT INTO domains (id, user_id, hostname, zone_id, status, created_at) VALUES (?, 'admin', ?, 'manual', 'active', 1)");
for (const alias of aliases) insert.run(alias.domainId, `${alias.domainId}.test`);
}
const prepare = f.database.prepare.bind(f.database);
t.mock.method(f.database, "prepare", (query) => {
const statement = prepare(query);
const bind = statement.bind.bind(statement);
statement.bind = (...values) => {
assert.ok(values.length <= 100, "each statement must fit D1's 100-parameter limit");
return bind(...values);
};
return statement;
});
assert.equal((await f.post("api", { useAllDomains: false, aliases })).status, 201);
assert.equal(f.database.db.prepare("SELECT count(*) AS count FROM mailbox_aliases").get().count, 125);
assert.equal((await getMailboxDomainAddresses(f.db, f.mailbox())).length, 126);
assert.equal(f.calls.length, 0);
});
}
test("the all-domain boolean rejects truthy strings", () => {
const input = { username: "sam", domainId: "one", password: "test-password" };
assert.equal(createUserAccountSchema.safeParse({ ...input, useAllDomains: "false" }).success, false);
});