mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
fix merge conflict
This commit is contained in:
commit
d045ee1600
38 changed files with 4938 additions and 643 deletions
155
tests/onboarding/onboarding-steps.test.ts
Normal file
155
tests/onboarding/onboarding-steps.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
ONBOARDING_STEPS,
|
||||
ONBOARDING_STEP_IDS,
|
||||
WIZARD_STEP_IDS,
|
||||
LINK_STEP_IDS,
|
||||
countCompletedSteps,
|
||||
firstOpenWizardStep,
|
||||
getStepStates,
|
||||
isOnboardingComplete,
|
||||
isOnboardingStepId,
|
||||
type OnboardingContext,
|
||||
} from "@/src/modules/onboarding/lib/onboarding-steps";
|
||||
|
||||
/** Frisch registriertes Studio: nichts eingerichtet. */
|
||||
const emptyContext: OnboardingContext = {
|
||||
stripeAccountStatus: null,
|
||||
slug: null,
|
||||
isPublic: false,
|
||||
hasBranding: false,
|
||||
hasCancellationPolicy: false,
|
||||
roomCount: 0,
|
||||
employeeCount: 0,
|
||||
courseCount: 0,
|
||||
};
|
||||
|
||||
/** Alle Pflichtschritte erledigt, optionale Schritte offen. */
|
||||
const requiredDoneContext: OnboardingContext = {
|
||||
...emptyContext,
|
||||
stripeAccountStatus: "active",
|
||||
slug: "mein-studio",
|
||||
isPublic: true,
|
||||
roomCount: 1,
|
||||
courseCount: 1,
|
||||
};
|
||||
|
||||
describe("isOnboardingStepId", () => {
|
||||
it("erkennt gültige Registry-Keys", () => {
|
||||
expect(isOnboardingStepId("payments")).toBe(true);
|
||||
expect(isOnboardingStepId("course")).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt unbekannte Keys ab", () => {
|
||||
expect(isOnboardingStepId("zahlungen")).toBe(false);
|
||||
expect(isOnboardingStepId("")).toBe(false);
|
||||
expect(isOnboardingStepId("__proto__")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Registry-Aufteilung", () => {
|
||||
it("teilt alle Schritte überschneidungsfrei in Wizard- und Link-Schritte", () => {
|
||||
expect([...WIZARD_STEP_IDS, ...LINK_STEP_IDS].sort()).toEqual([...ONBOARDING_STEP_IDS].sort());
|
||||
expect(WIZARD_STEP_IDS.filter((id) => (LINK_STEP_IDS as string[]).includes(id))).toEqual([]);
|
||||
});
|
||||
|
||||
it("hat mindestens einen Pflichtschritt", () => {
|
||||
expect(ONBOARDING_STEP_IDS.some((id) => ONBOARDING_STEPS[id].required)).toBe(true);
|
||||
});
|
||||
|
||||
it("verlinkt jeden Schritt auf einen app-internen Pfad", () => {
|
||||
for (const id of ONBOARDING_STEP_IDS) {
|
||||
expect(ONBOARDING_STEPS[id].href.startsWith("/")).toBe(true);
|
||||
expect(ONBOARDING_STEPS[id].href.startsWith("//")).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStepStates", () => {
|
||||
it("meldet im leeren Studio jeden Schritt als offen", () => {
|
||||
const states = getStepStates(emptyContext);
|
||||
expect(states).toHaveLength(ONBOARDING_STEP_IDS.length);
|
||||
expect(states.every((s) => !s.done)).toBe(true);
|
||||
});
|
||||
|
||||
it("behält die Registry-Reihenfolge bei", () => {
|
||||
expect(getStepStates(emptyContext).map((s) => s.id)).toEqual(ONBOARDING_STEP_IDS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ableitung der einzelnen Schritte", () => {
|
||||
it("hakt Zahlungen nur bei aktivem Stripe-Konto ab", () => {
|
||||
expect(
|
||||
ONBOARDING_STEPS.payments.isDone({ ...emptyContext, stripeAccountStatus: "active" })
|
||||
).toBe(true);
|
||||
// "pending" und "restricted" reichen NICHT: in beiden Fällen kann das Studio
|
||||
// kein Geld empfangen, der Schritt muss also offen bleiben.
|
||||
expect(
|
||||
ONBOARDING_STEPS.payments.isDone({ ...emptyContext, stripeAccountStatus: "pending" })
|
||||
).toBe(false);
|
||||
expect(
|
||||
ONBOARDING_STEPS.payments.isDone({ ...emptyContext, stripeAccountStatus: "restricted" })
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verlangt für die öffentliche Seite Slug UND Sichtbarkeit", () => {
|
||||
expect(
|
||||
ONBOARDING_STEPS.publicPage.isDone({ ...emptyContext, isPublic: true, slug: null })
|
||||
).toBe(false);
|
||||
expect(
|
||||
ONBOARDING_STEPS.publicPage.isDone({ ...emptyContext, isPublic: false, slug: "s" })
|
||||
).toBe(false);
|
||||
expect(ONBOARDING_STEPS.publicPage.isDone({ ...emptyContext, isPublic: true, slug: "s" })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOnboardingComplete", () => {
|
||||
it("ist im leeren Studio nicht erfüllt", () => {
|
||||
expect(isOnboardingComplete(emptyContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("ist erfüllt, sobald alle Pflichtschritte stehen - auch ohne die optionalen", () => {
|
||||
expect(isOnboardingComplete(requiredDoneContext)).toBe(true);
|
||||
expect(requiredDoneContext.hasBranding).toBe(false);
|
||||
expect(requiredDoneContext.employeeCount).toBe(0);
|
||||
});
|
||||
|
||||
it("fällt zurück, wenn ein Pflichtschritt wegbricht (z.B. Stripe wird restricted)", () => {
|
||||
expect(
|
||||
isOnboardingComplete({ ...requiredDoneContext, stripeAccountStatus: "restricted" })
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countCompletedSteps", () => {
|
||||
it("zählt über ALLE Schritte, nicht nur die Pflichtschritte", () => {
|
||||
expect(countCompletedSteps(emptyContext)).toBe(0);
|
||||
expect(countCompletedSteps(requiredDoneContext)).toBe(
|
||||
ONBOARDING_STEP_IDS.filter((id) => ONBOARDING_STEPS[id].required).length
|
||||
);
|
||||
expect(
|
||||
countCompletedSteps({ ...requiredDoneContext, hasBranding: true, employeeCount: 2 })
|
||||
).toBe(ONBOARDING_STEP_IDS.length - 1); // nur die Storno-Policy fehlt noch
|
||||
});
|
||||
});
|
||||
|
||||
describe("firstOpenWizardStep", () => {
|
||||
it("liefert den ersten offenen Wizard-Schritt in Registry-Reihenfolge", () => {
|
||||
expect(firstOpenWizardStep(emptyContext)).toBe(WIZARD_STEP_IDS[0]);
|
||||
});
|
||||
|
||||
it("überspringt bereits erledigte Schritte", () => {
|
||||
expect(firstOpenWizardStep({ ...emptyContext, hasBranding: true })).toBe(WIZARD_STEP_IDS[1]);
|
||||
});
|
||||
|
||||
it("liefert null, wenn alle Wizard-Schritte erledigt sind", () => {
|
||||
const allWizardDone: OnboardingContext = {
|
||||
...requiredDoneContext,
|
||||
hasBranding: true,
|
||||
hasCancellationPolicy: true,
|
||||
};
|
||||
expect(firstOpenWizardStep(allWizardDone)).toBeNull();
|
||||
});
|
||||
});
|
||||
27
tests/settings/return-to.test.ts
Normal file
27
tests/settings/return-to.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { sanitizeReturnTo } from "@/src/modules/settings/lib/return-to";
|
||||
|
||||
describe("sanitizeReturnTo", () => {
|
||||
it("lässt app-interne Pfade durch", () => {
|
||||
expect(sanitizeReturnTo("/onboarding")).toBe("/onboarding");
|
||||
expect(sanitizeReturnTo("/onboarding?step=payments")).toBe("/onboarding?step=payments");
|
||||
});
|
||||
|
||||
it("lehnt absolute URLs auf fremde Hosts ab", () => {
|
||||
expect(sanitizeReturnTo("https://evil.example/pwn")).toBeNull();
|
||||
expect(sanitizeReturnTo("http://evil.example")).toBeNull();
|
||||
});
|
||||
|
||||
it("lehnt protokollrelative URLs ab", () => {
|
||||
// "//host" und "/\host" interpretieren Browser als fremde Domain - das wäre
|
||||
// ein offener Redirect über den Stripe-Rücksprung.
|
||||
expect(sanitizeReturnTo("//evil.example")).toBeNull();
|
||||
expect(sanitizeReturnTo("/\\evil.example")).toBeNull();
|
||||
});
|
||||
|
||||
it("behandelt leere und fehlende Werte als 'kein returnTo'", () => {
|
||||
expect(sanitizeReturnTo(null)).toBeNull();
|
||||
expect(sanitizeReturnTo(undefined)).toBeNull();
|
||||
expect(sanitizeReturnTo("")).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue