first commit
Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s

This commit is contained in:
Nick Szaunig committed 2026-09-27 06:06:56 +02:00
commit 59bb5a38a3
132 files changed
+19435

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
import { beforeEach, describe, expect, test } from "vitest";
import { countOccupiedSeats, createBooking } from "@/lib/booking-service";
import { bookingRequest, db, EARLY_BIRD_NOW, resetDb, SECRET, setupCamp } from "./support/db";
const testCtx = { now: EARLY_BIRD_NOW, mode: "test" as const, secret: SECRET };
beforeEach(async () => {
await resetDb();
await setupCamp();
});
async function voucher(code: string, data: Partial<{ maxRedemptions: number; active: boolean; validUntil: Date; validFrom: Date; boundEmail: string }> = {}) {
return db.voucher.create({ data: { code, maxRedemptions: 1, ...data } });
}
describe("Kapazitaet", () => {
test("gleichzeitige Buchungen bei knapper Kapazitaet: nie mehr Zusagen als Plaetze", async () => {
await setupCamp({ capacity: 2 });
const results = await Promise.all(Array.from({ length: 8 }, () => createBooking(db, bookingRequest(), testCtx)));
const ok = results.filter((r) => r.ok);
const soldOut = results.filter((r) => !r.ok && r.error.code === "SOLD_OUT");
expect(ok).toHaveLength(2);
expect(soldOut).toHaveLength(6);
expect(await countOccupiedSeats(db, false, EARLY_BIRD_NOW)).toBe(2);
});
test("abgelaufene Reservierung gibt den Platz wieder frei", async () => {
await setupCamp({ capacity: 1 });
const first = await createBooking(db, bookingRequest(), testCtx);
expect(first.ok).toBe(true);
const blocked = await createBooking(db, bookingRequest(), testCtx);
expect(blocked.ok ? "ok" : blocked.error.code).toBe("SOLD_OUT");
// 45 Minuten spaeter: Stripe-Session (31 min) und Kulanzpuffer (10 min) vorbei.
const later = new Date(EARLY_BIRD_NOW.getTime() + 45 * 60_000);
const again = await createBooking(db, bookingRequest(), { ...testCtx, now: later });
expect(again.ok).toBe(true);
});
test("Testbuchungen belegen keine Live-Plaetze", async () => {
await setupCamp({ capacity: 1 });
await createBooking(db, bookingRequest(), testCtx);
expect(await countOccupiedSeats(db, true, EARLY_BIRD_NOW)).toBe(0);
});
test("doppelt abgeschicktes Formular erzeugt genau eine Buchung", async () => {
const request = bookingRequest();
const [a, b] = await Promise.all([createBooking(db, request, testCtx), createBooking(db, request, testCtx)]);
expect(a.ok && b.ok).toBe(true);
expect(await db.booking.count()).toBe(1);
});
});
describe("Preis und Snapshot", () => {
test("Early-Bird-Preis wird serverseitig berechnet und als Snapshot gespeichert", async () => {
const result = await createBooking(db, bookingRequest({ planType: "THREE_INSTALLMENTS" }), testCtx);
if (!result.ok) throw new Error(result.error.code);
expect(result.booking.totalCents).toBe(220_000);
expect(result.booking.installments.map((i) => i.amountCents)).toEqual([80_000, 70_000, 70_000]);
expect(result.booking.priceLockedUntil.getTime() - EARLY_BIRD_NOW.getTime()).toBe(31 * 60_000);
});
test("Preis-Snapshot und Raten sind per Datenbank-Trigger unveraenderlich", async () => {
const result = await createBooking(db, bookingRequest({ planType: "TWO_INSTALLMENTS" }), testCtx);
if (!result.ok) throw new Error(result.error.code);
await expect(db.booking.update({ where: { id: result.booking.id }, data: { totalCents: 1 } })).rejects.toThrow();
await expect(
db.installment.update({ where: { id: result.booking.installments[1]!.id }, data: { amountCents: 1 } }),
).rejects.toThrow();
// Statusfelder bleiben aenderbar.
await db.booking.update({ where: { id: result.booking.id }, data: { reviewReason: "ok" } });
});
test("Regulaerer Preis ab 01.01.2027 00:00 Berlin, Stammmodel 2.475 €", async () => {
await voucher("STAMM-REG");
const now = new Date("2026-12-31T23:00:00Z");
const result = await createBooking(db, bookingRequest({ voucherCode: "stamm-reg" }), { ...testCtx, now });
if (!result.ok) throw new Error(result.error.code);
expect(result.booking.pricePhase).toBe("REGULAR");
expect(result.booking.totalCents).toBe(247_500);
});
test("Ratenzahlung ohne Camp-Termin wird serverseitig abgelehnt", async () => {
await setupCamp({ campStartDate: null });
const result = await createBooking(db, bookingRequest({ planType: "TWO_INSTALLMENTS" }), testCtx);
expect(result.ok ? "ok" : result.error.code).toBe("PLAN_UNAVAILABLE");
});
});
describe("Gutscheine", () => {
test("gueltiger Code: Early Bird + Stammmodel = 1.980 €", async () => {
await voucher("STAMMMODELTEST1");
const result = await createBooking(db, bookingRequest({ voucherCode: " stammmodeltest1 " }), testCtx);
if (!result.ok) throw new Error(result.error.code);
expect(result.booking.totalCents).toBe(198_000);
expect(await db.voucherRedemption.count({ where: { status: "RESERVED" } })).toBe(1);
});
test.each([
["unbekannt", "GIBTESNICHT", {}, "UNKNOWN"],
["inaktiv", "CODE-INAKTIV", { active: false }, "INACTIVE"],
["abgelaufen", "CODE-ALT", { validUntil: new Date("2026-11-01T00:00:00Z") }, "EXPIRED"],
["noch nicht gueltig", "CODE-SPAET", { validFrom: new Date("2026-12-01T00:00:00Z") }, "NOT_YET_VALID"],
["an andere E-Mail gebunden", "CODE-BOUND", { boundEmail: "[email protected]" }, "EMAIL_MISMATCH"],
] as const)("%s wird abgelehnt", async (_label, code, data, reason) => {
if (reason !== "UNKNOWN") await voucher(code, data);
const result = await createBooking(db, bookingRequest({ voucherCode: code }), testCtx);
expect(result.ok).toBe(false);
if (!result.ok && result.error.code === "VOUCHER_REJECTED") expect(result.error.reason).toBe(reason);
expect(await db.booking.count()).toBe(0);
});
test("E-Mail-Bindung akzeptiert die richtige Adresse unabhaengig von Grossschreibung", async () => {
await voucher("CODE-MAIL", { boundEmail: "[email protected]" });
const result = await createBooking(db, bookingRequest({ voucherCode: "CODE-MAIL", email: "[email protected]" }), testCtx);
expect(result.ok).toBe(true);
});
test("ausgeschoepfter Code wird abgelehnt", async () => {
await voucher("CODE-EINMAL");
expect((await createBooking(db, bookingRequest({ voucherCode: "CODE-EINMAL" }), testCtx)).ok).toBe(true);
const second = await createBooking(db, bookingRequest({ voucherCode: "CODE-EINMAL" }), testCtx);
expect(second.ok ? "ok" : second.error.code === "VOUCHER_REJECTED" && second.error.reason).toBe("EXHAUSTED");
});
test("gleichzeitige Einloesung eines begrenzten Codes: genau so viele wie erlaubt", async () => {
await voucher("CODE-ZWEI", { maxRedemptions: 2 });
const results = await Promise.all(
Array.from({ length: 7 }, () => createBooking(db, bookingRequest({ voucherCode: "CODE-ZWEI" }), testCtx)),
);
expect(results.filter((r) => r.ok)).toHaveLength(2);
expect(await db.voucherRedemption.count()).toBe(2);
});
test("Gesamtkontingent fuer Stammmodel-Buchungen greift ueber alle Codes", async () => {
await setupCamp({ stammmodelQuota: 2 });
await voucher("CODE-A", { maxRedemptions: 5 });
await voucher("CODE-B", { maxRedemptions: 5 });
const results = await Promise.all([
createBooking(db, bookingRequest({ voucherCode: "CODE-A" }), testCtx),
createBooking(db, bookingRequest({ voucherCode: "CODE-B" }), testCtx),
createBooking(db, bookingRequest({ voucherCode: "CODE-A" }), testCtx),
createBooking(db, bookingRequest({ voucherCode: "CODE-B" }), testCtx),
]);
expect(results.filter((r) => r.ok)).toHaveLength(2);
const rejected = results.find((r) => !r.ok);
expect(rejected && !rejected.ok && rejected.error.code === "VOUCHER_REJECTED" && rejected.error.reason).toBe("QUOTA_EXHAUSTED");
});
test("abgelaufene Reservierung gibt die Einloesung wieder frei", async () => {
await voucher("CODE-FREI");
await createBooking(db, bookingRequest({ voucherCode: "CODE-FREI" }), testCtx);
const later = new Date(EARLY_BIRD_NOW.getTime() + 45 * 60_000);
const again = await createBooking(db, bookingRequest({ voucherCode: "CODE-FREI" }), { ...testCtx, now: later });
expect(again.ok).toBe(true);
});
});
describe("Verkaufsfreigabe im Buchungsservice", () => {
test("Livemodus ohne Freigaben: keine Buchung, auch bei direktem Aufruf", async () => {
const result = await createBooking(db, bookingRequest(), { ...testCtx, mode: "live" });
expect(result.ok ? "ok" : result.error.code).toBe("SALES_CLOSED");
expect(await db.booking.count()).toBe(0);
});
test("Vorschauphase: im Testmodus ohne Override gesperrt, mit Override buchbar", async () => {
const preview = new Date("2026-10-01T10:00:00Z");
const closed = await createBooking(db, bookingRequest(), { ...testCtx, now: preview });
expect(closed.ok ? "ok" : closed.error.code).toBe("SALES_CLOSED");
await setupCamp({ testPhaseOverride: "EARLY_BIRD" });
const open = await createBooking(db, bookingRequest(), { ...testCtx, now: preview });
expect(open.ok).toBe(true);
});
});