Test: Audit-Request-Kontext und Mail-Absender je Mandant
10 Prüfungen: IP/User-Agent null außerhalb eines Requests, Anzeigename und Reply-To des Mandanten, Plattform-Adresse bleibt, Header-Injection bereinigt, Rückfall auf Plattform-Defaults für Plattform-Mails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import "dotenv/config";
|
||||
import { prisma } from "../src/server/db";
|
||||
import { writeAuditLog } from "../src/server/audit";
|
||||
import { tenantSender } from "../src/server/mail/deliver";
|
||||
import type { MailConfig } from "../src/server/mail/config";
|
||||
|
||||
/**
|
||||
* Foundation fixes:
|
||||
* (a) writeAuditLog outside a request scope stores ip/userAgent = null (no crash)
|
||||
* (b) mail sender uses the tenant display name + reply-to, keeps the platform address,
|
||||
* strips header-injection characters, and falls back for platform mails.
|
||||
*/
|
||||
|
||||
let failures = 0;
|
||||
function check(name: string, cond: boolean, detail?: unknown) {
|
||||
if (cond) console.log(` ✓ ${name}`);
|
||||
else {
|
||||
failures++;
|
||||
console.log(` ✗ ${name}`, detail ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
const config: MailConfig = {
|
||||
host: "localhost",
|
||||
port: 1025,
|
||||
secure: false,
|
||||
from: "no-reply@craftvia.test",
|
||||
fromName: "Craftvia",
|
||||
replyTo: "support@craftvia.test",
|
||||
baseUrl: "http://localhost:3000",
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const slug = `zz-amc-${Date.now()}`;
|
||||
const tenant = await prisma.tenant.create({ data: { name: "ZZ Audit Mail", slug } });
|
||||
try {
|
||||
// (a) audit outside request
|
||||
await writeAuditLog({ tenantId: tenant.id, action: "update", entity: "zz_test", entityId: "x1" });
|
||||
const row = await prisma.auditLog.findFirst({ where: { tenantId: tenant.id, entity: "zz_test" } });
|
||||
check("audit row written outside request", !!row);
|
||||
check("ipAddress null outside request", row?.ipAddress === null, row?.ipAddress);
|
||||
check("userAgent null outside request", row?.userAgent === null, row?.userAgent);
|
||||
|
||||
// (b) tenant sender
|
||||
await prisma.tenantSettings.create({
|
||||
data: { tenantId: tenant.id, orgName: "ZZ", mailFromName: 'Musterbau "Evil"\r\nBcc: x@y.z <GmbH>', mailReplyTo: "buero@musterbau.test" },
|
||||
});
|
||||
const log = await prisma.mailLog.create({ data: { tenantId: tenant.id, to: "a@b.test", template: "test" } });
|
||||
const s = await tenantSender(log.id, config);
|
||||
check("platform address kept", s.from.endsWith("<no-reply@craftvia.test>"), s.from);
|
||||
check("tenant display name used", s.from.startsWith("Musterbau Evil"), s.from);
|
||||
check("no CR/LF or quotes in From", !/[\r\n"]/.test(s.from), JSON.stringify(s.from));
|
||||
check("only one angle-bracket address", (s.from.match(/</g) ?? []).length === 1, s.from);
|
||||
check("tenant reply-to used", s.replyTo === "buero@musterbau.test", s.replyTo);
|
||||
|
||||
const platformLog = await prisma.mailLog.create({ data: { tenantId: null, scope: "platform", to: "a@b.test", template: "test" } });
|
||||
const p = await tenantSender(platformLog.id, config);
|
||||
check("platform mail falls back to default name", p.from === "Craftvia <no-reply@craftvia.test>", p.from);
|
||||
check("platform mail falls back to default reply-to", p.replyTo === "support@craftvia.test", p.replyTo);
|
||||
await prisma.mailLog.delete({ where: { id: platformLog.id } });
|
||||
} finally {
|
||||
await prisma.mailLog.deleteMany({ where: { tenantId: tenant.id } });
|
||||
await prisma.auditLog.deleteMany({ where: { tenantId: tenant.id } });
|
||||
await prisma.tenantSettings.deleteMany({ where: { tenantId: tenant.id } });
|
||||
await prisma.tenant.delete({ where: { id: tenant.id } });
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
console.log(failures ? `\n${failures} Fehler` : "\nOK");
|
||||
process.exit(failures ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export async function markMailFailed(mailLogId: string, error: string): Promise<
|
||||
* the platform address (SPF/DKIM alignment); only the display name and Reply-To vary.
|
||||
* Raw client on purpose: the worker has no tenant context; the MailLog row carries the tenant.
|
||||
*/
|
||||
async function tenantSender(mailLogId: string, config: MailConfig): Promise<{ from: string; replyTo?: string }> {
|
||||
export async function tenantSender(mailLogId: string, config: MailConfig): Promise<{ from: string; replyTo?: string }> {
|
||||
const log = await prisma.mailLog.findUnique({ where: { id: mailLogId }, select: { tenantId: true } });
|
||||
if (!log?.tenantId) return { from: mailFrom(config), replyTo: config.replyTo };
|
||||
const settings = await prisma.tenantSettings.findUnique({
|
||||
|
||||
Reference in New Issue
Block a user