Files
craftvia/scripts/smoke-testphase.ts

156 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* L15 Testphase — HTTP smoke against a running server, WITHOUT typing passwords (session cookies are
* built like scripts/smoke-auth.ts). Creates zz trial tenants (expired + running) through the real
* services, checks the public wizard pages, banners, the "Erste Schritte" card, the export page, the
* central write lock on real /api/v1 routes (customers, sync, uploads, work-order documents,
* backoffice upload) and the platform pages; removes the zz tenants afterwards.
*
* Usage: BASE=http://localhost:3115 npx tsx scripts/smoke-testphase.ts
*/
import "dotenv/config";
import { encode } from "next-auth/jwt";
import { prisma } from "../src/server/db";
import { finalizeIdentityLogin } from "../src/server/auth";
import { addDaysToKey, todayKey } from "../src/lib/trial/dates";
import { endTrialNow } from "../src/server/services/trial/admin";
import { addMember, cleanupL15, platformAdmin, trialTenant } from "./lib/testphase-fixture";
const BASE = process.env.BASE ?? "http://localhost:3115";
const SECURE = BASE.startsWith("https");
const COOKIE = SECURE ? "__Secure-authjs.session-token" : "authjs.session-token";
const PLATFORM_COOKIE = `${SECURE ? "__Secure-" : ""}platform-authjs.session-token`;
type Check = { label?: string; path: string; method?: string; body?: BodyInit; headers?: Record<string, string>; expect?: number[]; mustContain?: string[]; mustNotContain?: string[]; redirectTo?: string };
async function tenantCookie(email: string, slug: string): Promise<string> {
const identity = await prisma.identity.findUniqueOrThrow({ where: { email } });
const user = await finalizeIdentityLogin(identity.id, slug);
if (!user) throw new Error(`no membership for ${email} in ${slug}`);
const token = {
sub: user.id, name: user.name, email: user.email, userId: user.id, identityId: user.identityId, tenantId: user.tenantId, tenantSlug: user.tenantSlug,
activeMembershipId: user.activeMembershipId, memberships: user.memberships, roles: user.roles, permissions: user.permissions, isPlatformAdmin: user.isPlatformAdmin, mfaEnrolled: user.mfaEnrolled,
};
return `${COOKIE}=${await encode({ token, secret: process.env.AUTH_SECRET!, salt: COOKIE, maxAge: 1800 })}`;
}
async function platformCookie(adminId: string): Promise<string> {
const token = { sub: adminId, userId: adminId, isPlatformAdmin: true, mfaEnrolled: false };
return `${PLATFORM_COOKIE}=${await encode({ token, secret: process.env.AUTH_SECRET!, salt: PLATFORM_COOKIE, maxAge: 1800 })}`;
}
function multipart(fields: Record<string, string>, file?: { name: string; type: string; bytes: Buffer }): FormData {
const fd = new FormData();
for (const [k, v] of Object.entries(fields)) fd.set(k, v);
if (file) fd.set("file", new Blob([new Uint8Array(file.bytes)], { type: file.type }), file.name);
return fd;
}
async function main() {
await cleanupL15();
const today = todayKey();
const expired = await trialTenant("Smoke Abgelaufen", addDaysToKey(today, 5), { sampleData: true });
const running = await trialTenant("Smoke Laufend", addDaysToKey(today, 3), { sampleData: true });
const tech = await addMember(expired.tenantId, "tech-smoke", "technician");
const admin = await platformAdmin("full");
await endTrialNow({ platformAdminId: admin.id }, expired.tenantId);
const expiredSlug = (await prisma.tenant.findUniqueOrThrow({ where: { id: expired.tenantId } })).slug;
const runningSlug = (await prisma.tenant.findUniqueOrThrow({ where: { id: running.tenantId } })).slug;
const order = await prisma.workOrder.findFirstOrThrow({ where: { tenantId: expired.tenantId, status: "assigned" } });
const pdf = Buffer.from("%PDF-1.4\n%%EOF\n");
const plans: { who: string; cookie: string; checks: Check[] }[] = [
{
who: "anonym",
cookie: "",
checks: [
{ path: "/testen", mustContain: ["Craftvia kostenlos testen", "Firmenname", "Schritt 1 von 5", "Nutzungsbedingungen"] },
{ path: "/testen/bestaetigen?token=ungueltig", mustContain: ["ungültig"] },
{ path: "/testen/nutzungsbedingungen", mustContain: ["Nutzungsbedingungen", "Platzhalter"] },
{ path: "/testen/datenschutz", mustContain: ["Datenschutz"] },
{ path: "/dashboard", expect: [307], redirectTo: "/login" },
{ path: "/settings/export", expect: [307], redirectTo: "/login" },
],
},
{
who: `Admin abgelaufen (${expiredSlug})`,
cookie: await tenantCookie(`admin-smoke-abgelaufen@zz-l15.test`, expiredSlug),
checks: [
{ path: "/dashboard", mustContain: ["Testphase abgelaufen – nur Lesezugriff.", "Daten werden am", "Daten exportieren", 'data-trial-banner="expired"'] },
{ path: "/customers", mustContain: ["Testphase abgelaufen", "Hausverwaltung Musterhof"] },
{ path: "/settings/export", mustContain: ["Datenexport", "Export erstellen", "auch nach Ablauf"] },
{ path: "/api/v1/customers", mustContain: ['"data"'] },
{ label: "POST /api/v1/customers → gesperrt", path: "/api/v1/customers", method: "POST", body: JSON.stringify({ companyName: "ZZ Smoke" }), headers: { "content-type": "application/json" }, expect: [422], mustContain: ["trial_expired", "nur Lesezugriff"] },
{ label: "POST /api/v1/work-orders/[id]/documents → gesperrt", path: `/api/v1/work-orders/${order.id}/documents`, method: "POST", body: multipart({ category: "other", visibility: "team" }, { name: "a.pdf", type: "application/pdf", bytes: pdf }), headers: { accept: "application/json" }, expect: [422], mustContain: ["trial_expired"] },
{ label: "POST /documents/upload → gesperrt", path: "/documents/upload", method: "POST", body: multipart({ category: "other", visibility: "team" }, { name: "a.pdf", type: "application/pdf", bytes: pdf }), headers: { accept: "application/json" }, expect: [422], mustContain: ["trial_expired"] },
{ path: "/settings/export/unbekannt", expect: [404] },
],
},
{
who: `Monteur abgelaufen (${expiredSlug})`,
cookie: await tenantCookie(tech.email, expiredSlug),
checks: [
{ path: "/m", mustContain: ["Testphase abgelaufen – nur Lesezugriff.", 'data-trial-banner="expired"'] },
{ path: "/m/orders", mustContain: ["Testphase abgelaufen – nur Lesezugriff."] },
{ label: "GET /m/orders/[id] ohne Zuweisung → 404 (Scope)", path: `/m/orders/${order.id}`, expect: [404] },
{ path: "/m/emergency", mustContain: ["Testphase abgelaufen – nur Lesezugriff."] },
{ path: "/api/v1/field/bundle", mustContain: ['"orders"'] },
{ label: "POST /api/v1/sync → gesperrt", path: "/api/v1/sync", method: "POST", body: JSON.stringify({ deviceId: "zz-smoke", operations: [] }), headers: { "content-type": "application/json" }, expect: [422], mustContain: ["trial_expired"] },
{ label: "POST /api/v1/uploads → gesperrt", path: "/api/v1/uploads", method: "POST", body: multipart({ clientId: "7c1d6a0e-3b1f-4c55-9d2a-00000000f016", workOrderId: order.id, kind: "photo" }, { name: "a.jpg", type: "image/jpeg", bytes: Buffer.from([0xff, 0xd8, 0xff, 0xd9]) }), expect: [422], mustContain: ["trial_expired"] },
{ path: "/settings/export", expect: [307], redirectTo: "/dashboard" },
],
},
{
who: `Admin laufend (${runningSlug})`,
cookie: await tenantCookie(`admin-smoke-laufend@zz-l15.test`, runningSlug),
checks: [
{ path: "/dashboard", mustContain: ["Testphase endet in 3 Tagen.", "Erste Schritte", "von 5 erledigt", "Team anlegen", "Monteur einladen"] },
{ path: "/dashboard?welcome=1", mustContain: ["Willkommen! Ihre Testphase ist eingerichtet."] },
{ path: "/settings/export", mustContain: ["Datenexport"] },
],
},
{
who: "Plattform-Admin",
cookie: await platformCookie(admin.id),
checks: [
{ path: "/admin", mustContain: ["Testmandant anlegen", "ZZ L15 Smoke Abgelaufen", "abgelaufen – nur lesen", "Löschung am", "Test bis"] },
{ path: "/admin?plan=trial", mustContain: ["ZZ L15 Smoke Laufend"], mustNotContain: ["Musterbau Haustechnik"] },
{ path: "/admin?plan=full", mustNotContain: ["ZZ L15 Smoke Laufend"] },
{ path: "/admin/trial", mustContain: ["Testmandant anlegen", "Testphase bis", "Mit Beispieldaten"] },
{ path: `/admin/${expired.tenantId}`, mustContain: ["Testphase", "abgelaufen – nur lesen", "In Vollversion umwandeln", "Enddatum ändern / verlängern", "Löschung abbrechen"] },
{ path: `/admin/${expired.tenantId}?trial=extend`, mustContain: ["Enddatum ändern", "Ich bestätige diese Änderung."] },
{ path: `/admin/${running.tenantId}?trial=end`, mustContain: ["Testphase sofort beenden"] },
],
},
];
let failures = 0;
let total = 0;
for (const plan of plans) {
console.log(`\n== ${plan.who}`);
for (const c of plan.checks) {
total++;
const res = await fetch(BASE + c.path, { method: c.method ?? "GET", body: c.body, headers: { ...(plan.cookie ? { cookie: plan.cookie } : {}), ...c.headers }, redirect: "manual", signal: AbortSignal.timeout(120_000) });
const body = res.status >= 300 && res.status < 400 ? "" : await res.text();
const expect = c.expect ?? [200];
const loc = res.headers.get("location");
const okRedirect = !c.redirectTo || (loc ? new URL(loc, BASE).pathname === c.redirectTo : false);
const errorPage = res.status === 200 && /Application error|Internal Server Error|Unhandled Runtime Error/i.test(body);
const missing = (c.mustContain ?? []).filter((s) => !body.includes(s));
const leaked = (c.mustNotContain ?? []).filter((s) => body.includes(s));
const pass = expect.includes(res.status) && okRedirect && !errorPage && missing.length === 0 && leaked.length === 0;
if (!pass) failures++;
console.log(`${pass ? "✓" : "✗"} ${String(res.status).padEnd(3)} ${c.label ?? `${c.method ?? "GET"} ${c.path}`}${loc ? ` → ${loc}` : ""}${missing.length ? ` [fehlt: ${missing.join(" | ")}]` : ""}${leaked.length ? ` [unerwartet: ${leaked.join(" | ")}]` : ""}`);
}
}
await cleanupL15();
await prisma.$disconnect();
console.log(failures ? `\n${failures} von ${total} Prüfungen fehlgeschlagen` : `\nOK — ${total} Prüfungen`);
process.exit(failures ? 1 : 0);
}
main().catch(async (err) => {
console.error(err);
await cleanupL15().catch(() => undefined);
process.exit(1);
});