L10a Qualität & Abnahmetests: HTTP-Sicherheitstest (manipulierte IDs aller /api/v1-Routen, Datei-Routen, Uploads, CSRF, Sessions, Login-Sperre, Header) gegen next start

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:34:29 +02:00
co-authored by Claude Opus 5
parent c297cfbe83
commit 388ca85f54
2 changed files with 396 additions and 5 deletions
+13 -5
View File
@@ -9,7 +9,8 @@
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { prisma } from "../src/server/db";
import type { SyncOperationInput } from "../src/lib/sync/envelope";
import { CONFLICTING_OPS, SYNC_OP_TYPES, type SyncOperationInput } from "../src/lib/sync/envelope";
import { EXTERNAL_OPS } from "../src/server/services/sync/external-ops";
import { applyOperations } from "../src/server/services/sync/apply";
import { storeFieldUpload } from "../src/server/services/field/uploads";
import { getFieldBundle } from "../src/server/services/field/queries";
@@ -137,10 +138,17 @@ runSuite("E2E Offline-Sync", [SLUG_A, SLUG_B], async () => {
section("7. Ungültige Ops, Scope, Mandantentrennung");
const invalid = await applyOperations(tech, { deviceId: "e2e-device", operations: [op("note.create", { workOrderId: w, text: "" })] });
ok(invalid.results[0].status === "rejected" && invalid.results[0].errorCode === "invalid", "ungültige Payload → rejected invalid");
// signature.capture is not registered in services/sync/external-ops.ts (reports lane uses server actions)
const unavailable = op("signature.capture", { reportId: "x" });
const na = await applyOperations(tech, { deviceId: "e2e-device", operations: [unavailable] });
ok(na.results[0].status === "rejected" && (await prisma.syncOperation.count({ where: { clientOpId: unavailable.clientOpId } })) === 0, "nicht verfügbare Op → rejected, nicht gespeichert (später wiederholbar)");
// An op type without server handler (neither field handler nor registered in services/sync/external-ops.ts)
// must be answered without being stored, so devices can retry after the owning lane is deployed.
const FIELD_OPS = ["session.start", "session.pause", "session.resume", "session.end", "work_order.transition", "note.create", "checklist.toggle", "material.upsert", "photo.attach", "voice.attach"];
const unregistered = SYNC_OP_TYPES.find((t) => !FIELD_OPS.includes(t) && !EXTERNAL_OPS[t] && !CONFLICTING_OPS.includes(t));
if (unregistered) {
const unavailable = op(unregistered, { reportId: "x" });
const na = await applyOperations(tech, { deviceId: "e2e-device", operations: [unavailable] });
ok(na.results[0].status === "rejected" && (await prisma.syncOperation.count({ where: { clientOpId: unavailable.clientOpId } })) === 0, `nicht verfügbare Op ${unregistered} → rejected, nicht gespeichert (später wiederholbar)`);
} else {
console.log("• alle Sync-Op-Typen sind registriert — Prüfung „nicht verfügbare Op“ entfällt");
}
const notesBefore = await prisma.activityNote.count({ where: { workOrderId: w } });
const outsider = await applyOperations(A.ctx.outsider, { deviceId: "o", operations: [op("note.create", { workOrderId: w, text: "fremd" }), op("session.start", { workOrderId: w, mode: "work" })] });
ok(outsider.results.every((x) => x.status === "rejected" && x.errorCode === "not_found"), "Monteur ohne Zuweisung → not_found");