mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 19:51:17 +00:00
added unit test framework
This commit is contained in:
parent
d3791060d2
commit
3c9fc19a3c
22 changed files with 2206 additions and 15 deletions
4
.husky/pre-push
Normal file
4
.husky/pre-push
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run test:run
|
||||
851
package-lock.json
generated
851
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,9 @@
|
|||
"prepare": "husky",
|
||||
"prettier:check": "prettier --check .",
|
||||
"prettier:fix": "prettier --write .",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.19.0",
|
||||
|
|
@ -49,6 +51,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
@ -57,11 +60,13 @@
|
|||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^4.0.6",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-config-next": "16.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.1.0",
|
||||
"lint-staged": "^16.2.6",
|
||||
"msw": "^2.13.6",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("Placeholder CI Test", () => {
|
||||
it("should always pass", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
24
tests/__mocks__/prisma.ts
Normal file
24
tests/__mocks__/prisma.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { vi } from "vitest";
|
||||
|
||||
export const mockPrisma = {
|
||||
manager: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
employee: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
$disconnect: vi.fn(),
|
||||
};
|
||||
28
tests/__mocks__/supabase.ts
Normal file
28
tests/__mocks__/supabase.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { vi } from "vitest";
|
||||
|
||||
export const mockAuth = {
|
||||
signInWithPassword: vi.fn(),
|
||||
signUp: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
};
|
||||
|
||||
export const mockStorage = {
|
||||
from: vi.fn().mockReturnValue({
|
||||
upload: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
getPublicUrl: vi
|
||||
.fn()
|
||||
.mockReturnValue({ data: { publicUrl: "https://example.com/avatar.jpg" } }),
|
||||
}),
|
||||
};
|
||||
|
||||
export const mockSupabaseClient = {
|
||||
auth: mockAuth,
|
||||
storage: mockStorage,
|
||||
};
|
||||
|
||||
// Used by vi.mock('@/src/lib/supabase/server', ...)
|
||||
export const createClient = vi.fn().mockResolvedValue(mockSupabaseClient);
|
||||
|
||||
// Used by vi.mock('@/src/lib/supabase/client', ...)
|
||||
export const createClientClient = vi.fn().mockReturnValue(mockSupabaseClient);
|
||||
65
tests/api/auth/login.test.ts
Normal file
65
tests/api/auth/login.test.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/src/app/api/auth/login/route";
|
||||
|
||||
vi.mock("@/src/lib/supabase/server", () => ({
|
||||
createClient: vi.fn(),
|
||||
}));
|
||||
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
|
||||
const mockSignInWithPassword = vi.fn();
|
||||
const mockSupabase = { auth: { signInWithPassword: mockSignInWithPassword } };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
||||
mockSignInWithPassword.mockReset();
|
||||
});
|
||||
|
||||
function makeRequest(body: Record<string, unknown>) {
|
||||
return new Request("http://localhost/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
describe("POST /api/auth/login", () => {
|
||||
it("gibt 400 zurück wenn E-Mail fehlt", async () => {
|
||||
const res = await POST(makeRequest({ password: "passwort123" }));
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.error).toBeDefined();
|
||||
});
|
||||
|
||||
it("gibt 400 zurück wenn Passwort fehlt", async () => {
|
||||
const res = await POST(makeRequest({ email: "test@example.com" }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei falschen Credentials", async () => {
|
||||
mockSignInWithPassword.mockResolvedValue({
|
||||
data: { user: null, session: null },
|
||||
error: { message: "Invalid login credentials" },
|
||||
});
|
||||
|
||||
const res = await POST(makeRequest({ email: "test@example.com", password: "falsch123" }));
|
||||
expect(res.status).toBe(401);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("falsch");
|
||||
});
|
||||
|
||||
it("gibt 200 und User/Session zurück bei Erfolg", async () => {
|
||||
const fakeUser = { id: "user-1", email: "test@example.com" };
|
||||
const fakeSession = { access_token: "token-abc" };
|
||||
mockSignInWithPassword.mockResolvedValue({
|
||||
data: { user: fakeUser, session: fakeSession },
|
||||
error: null,
|
||||
});
|
||||
|
||||
const res = await POST(makeRequest({ email: "test@example.com", password: "Richtig!123" }));
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.user.id).toBe("user-1");
|
||||
expect(data.session.access_token).toBe("token-abc");
|
||||
});
|
||||
});
|
||||
124
tests/api/auth/register-employee.test.ts
Normal file
124
tests/api/auth/register-employee.test.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/src/app/api/auth/register/employee/route";
|
||||
|
||||
vi.mock("@/src/lib/supabase/server", () => ({
|
||||
createClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: {
|
||||
employee: { findUnique: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
|
||||
const mockSignUp = vi.fn();
|
||||
const mockSupabase = { auth: { signUp: mockSignUp } };
|
||||
|
||||
const validToken = "valid-token-abc";
|
||||
const validEmployee = {
|
||||
email: "employee@example.com",
|
||||
isOnboarded: false,
|
||||
onboardingTokenExpiry: new Date(Date.now() + 3600000),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
||||
vi.mocked(prisma.employee.findUnique).mockReset();
|
||||
mockSignUp.mockReset();
|
||||
});
|
||||
|
||||
const validBody = {
|
||||
token: validToken,
|
||||
email: "employee@example.com",
|
||||
password: "Sicher!123",
|
||||
firstName: "Anna",
|
||||
lastName: "Schmidt",
|
||||
};
|
||||
|
||||
let ipCounter = 0;
|
||||
|
||||
function makeRequest(body: Record<string, unknown>) {
|
||||
// Each test gets a unique IP to avoid triggering the in-memory rate limiter
|
||||
const ip = `10.0.0.${++ipCounter}`;
|
||||
return new Request("http://localhost/api/auth/register/employee", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-forwarded-for": ip,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
describe("POST /api/auth/register/employee", () => {
|
||||
it("gibt 400 zurück wenn Pflichtfelder fehlen", async () => {
|
||||
const res = await POST(makeRequest({ token: validToken }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei ungültigem Token", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(null);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(401);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("Ungültiger Token");
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei bereits abgeschlossenem Onboarding", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue({
|
||||
...validEmployee,
|
||||
isOnboarded: true,
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei falschem E-Mail im Token", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue({
|
||||
...validEmployee,
|
||||
email: "andere@example.com",
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei abgelaufenem Token", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue({
|
||||
...validEmployee,
|
||||
onboardingTokenExpiry: new Date(Date.now() - 1000),
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(401);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("abgelaufen");
|
||||
});
|
||||
|
||||
it("gibt 400 zurück bei schwachem Passwort", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(validEmployee as never);
|
||||
|
||||
const res = await POST(makeRequest({ ...validBody, password: "schwach" }));
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("Passwort");
|
||||
});
|
||||
|
||||
it("gibt 201 zurück bei erfolgreicher Registrierung", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(validEmployee as never);
|
||||
mockSignUp.mockResolvedValue({
|
||||
data: { user: { id: "emp-1" } },
|
||||
error: null,
|
||||
});
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(201);
|
||||
const data = await res.json();
|
||||
expect(data.success).toBe(true);
|
||||
});
|
||||
});
|
||||
106
tests/api/auth/register.test.ts
Normal file
106
tests/api/auth/register.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/src/app/api/auth/register/route";
|
||||
|
||||
vi.mock("@/src/lib/supabase/server", () => ({
|
||||
createClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: {
|
||||
manager: { findUnique: vi.fn() },
|
||||
user: { findUnique: vi.fn(), create: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
|
||||
const mockSignUp = vi.fn();
|
||||
const mockSupabase = { auth: { signUp: mockSignUp } };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
||||
vi.mocked(prisma.manager.findUnique).mockReset();
|
||||
vi.mocked(prisma.user.findUnique).mockReset();
|
||||
vi.mocked(prisma.user.create).mockReset();
|
||||
mockSignUp.mockReset();
|
||||
});
|
||||
|
||||
const validBody = {
|
||||
email: "neu@example.com",
|
||||
password: "Sicher!123",
|
||||
name: "Max Mustermann",
|
||||
birthday: "1990-05-15",
|
||||
plz: "10115",
|
||||
city: "Berlin",
|
||||
street: "Musterstraße 1",
|
||||
houseNumber: "1",
|
||||
gender: "Men",
|
||||
};
|
||||
|
||||
function makeRequest(body: Record<string, unknown>) {
|
||||
return new Request("http://localhost/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
describe("POST /api/auth/register", () => {
|
||||
it("gibt 400 zurück wenn Pflichtfelder fehlen", async () => {
|
||||
const res = await POST(makeRequest({ email: "test@example.com" }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("gibt 409 zurück wenn E-Mail bereits als Manager existiert", async () => {
|
||||
vi.mocked(prisma.manager.findUnique).mockResolvedValue({
|
||||
id: "m-1",
|
||||
email: validBody.email,
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(409);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("Manager");
|
||||
});
|
||||
|
||||
it("gibt 409 zurück wenn User bereits existiert", async () => {
|
||||
vi.mocked(prisma.manager.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
id: "u-1",
|
||||
email: validBody.email,
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(409);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("existiert bereits");
|
||||
});
|
||||
|
||||
it("gibt 400 zurück bei Supabase-Auth-Fehler", async () => {
|
||||
vi.mocked(prisma.manager.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||
mockSignUp.mockResolvedValue({
|
||||
data: { user: null },
|
||||
error: { message: "Email rate limit exceeded" },
|
||||
});
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("gibt 201 und neuen User zurück bei Erfolg", async () => {
|
||||
const fakeUser = { id: "new-1" };
|
||||
const createdUser = { id: "new-1", email: validBody.email, name: validBody.name };
|
||||
|
||||
vi.mocked(prisma.manager.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||
mockSignUp.mockResolvedValue({ data: { user: fakeUser }, error: null });
|
||||
vi.mocked(prisma.user.create).mockResolvedValue(createdUser as never);
|
||||
|
||||
const res = await POST(makeRequest(validBody));
|
||||
expect(res.status).toBe(201);
|
||||
const data = await res.json();
|
||||
expect(data.user.email).toBe(validBody.email);
|
||||
});
|
||||
});
|
||||
136
tests/api/upload/avatar.test.ts
Normal file
136
tests/api/upload/avatar.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/src/app/api/upload/avatar/route";
|
||||
|
||||
vi.mock("@/src/lib/supabase/admin", () => ({
|
||||
createAdminClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: {
|
||||
employee: { findUnique: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
import { createAdminClient } from "@/src/lib/supabase/admin";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
|
||||
const mockUpload = vi.fn();
|
||||
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
||||
data: { publicUrl: "https://example.com/avatars/emp-1/photo.jpg" },
|
||||
});
|
||||
const mockStorageBucket = { upload: mockUpload, getPublicUrl: mockGetPublicUrl };
|
||||
const mockAdminClient = { storage: { from: vi.fn().mockReturnValue(mockStorageBucket) } };
|
||||
|
||||
const validEmployee = {
|
||||
id: "emp-1",
|
||||
isOnboarded: false,
|
||||
onboardingTokenExpiry: new Date(Date.now() + 3600000),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(createAdminClient).mockReturnValue(mockAdminClient as never);
|
||||
vi.mocked(prisma.employee.findUnique).mockReset();
|
||||
mockUpload.mockReset();
|
||||
mockUpload.mockResolvedValue({ data: { path: "employees/emp-1/photo.jpg" }, error: null });
|
||||
});
|
||||
|
||||
function makeImageFile(name = "photo.jpg", type = "image/jpeg", sizeBytes = 1024): File {
|
||||
const content = new Uint8Array(sizeBytes);
|
||||
const file = new File([content], name, { type });
|
||||
// Ensure arrayBuffer() is available (jsdom File may lack it in Node.js test env)
|
||||
if (typeof file.arrayBuffer !== "function") {
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: async () => content.buffer,
|
||||
});
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
// jsdom's multipart body parser drops File.type, so we mock formData() directly
|
||||
// to return a FormData containing the original File object unchanged.
|
||||
function makeRequest(file: File | null, token: string | null): Request {
|
||||
const req = new Request("http://localhost/api/upload/avatar", { method: "POST" });
|
||||
|
||||
req.formData = async () => {
|
||||
const fd = {
|
||||
get(key: string) {
|
||||
if (key === "file") return file;
|
||||
if (key === "token") return token;
|
||||
return null;
|
||||
},
|
||||
} as unknown as FormData;
|
||||
return fd;
|
||||
};
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
describe("POST /api/upload/avatar", () => {
|
||||
it("gibt 400 zurück wenn Datei fehlt", async () => {
|
||||
const res = await POST(makeRequest(null, "valid-token"));
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("Token");
|
||||
});
|
||||
|
||||
it("gibt 400 zurück wenn Token fehlt", async () => {
|
||||
const res = await POST(makeRequest(makeImageFile(), null));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei ungültigem Token", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(null);
|
||||
|
||||
const res = await POST(makeRequest(makeImageFile(), "invalid-token"));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei bereits abgeschlossenem Onboarding", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue({
|
||||
...validEmployee,
|
||||
isOnboarded: true,
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(makeImageFile(), "valid-token"));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("gibt 401 zurück bei abgelaufenem Token", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue({
|
||||
...validEmployee,
|
||||
onboardingTokenExpiry: new Date(Date.now() - 1000),
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest(makeImageFile(), "valid-token"));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("gibt 400 zurück bei nicht-Bilddatei", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(validEmployee as never);
|
||||
|
||||
const pdfFile = new File(["pdf content"], "doc.pdf", { type: "application/pdf" });
|
||||
const res = await POST(makeRequest(pdfFile, "valid-token"));
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("Bilddateien");
|
||||
});
|
||||
|
||||
it("gibt 400 zurück bei zu großer Datei (>5MB)", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(validEmployee as never);
|
||||
|
||||
const bigFile = makeImageFile("big.jpg", "image/jpeg", 6 * 1024 * 1024);
|
||||
const res = await POST(makeRequest(bigFile, "valid-token"));
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.error).toContain("5MB");
|
||||
});
|
||||
|
||||
it("gibt 201 und publicUrl zurück bei Erfolg", async () => {
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(validEmployee as never);
|
||||
|
||||
const res = await POST(makeRequest(makeImageFile(), "valid-token"));
|
||||
expect(res.status).toBe(201);
|
||||
const data = await res.json();
|
||||
expect(data.publicUrl).toContain("https://");
|
||||
});
|
||||
});
|
||||
163
tests/hooks/useAuth.test.ts
Normal file
163
tests/hooks/useAuth.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: mockRouterPush }),
|
||||
}));
|
||||
|
||||
vi.mock("@/src/store/authStore", () => ({
|
||||
useAuthStore: (selector: (s: { setUser: typeof mockSetUser }) => typeof mockSetUser) =>
|
||||
selector({ setUser: mockSetUser }),
|
||||
}));
|
||||
|
||||
vi.mock("@/src/lib/supabase/client", () => ({
|
||||
createClient: () => ({ auth: { signInWithPassword: mockSupabaseSignIn } }),
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
const mockRouterPush = vi.fn();
|
||||
const mockSetUser = vi.fn();
|
||||
const mockSupabaseSignIn = vi.fn();
|
||||
|
||||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouterPush.mockReset();
|
||||
mockSetUser.mockReset();
|
||||
mockSupabaseSignIn.mockReset();
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
});
|
||||
|
||||
describe("useAuth", () => {
|
||||
describe("loginUser", () => {
|
||||
it("gibt Zod-Validierungsfehler bei ungültiger E-Mail zurück", async () => {
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.loginUser("keine-email", "passwort123", false);
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(result.current.errors.email).toBeDefined();
|
||||
expect(mockSupabaseSignIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("setzt Fehler bei falschen Credentials", async () => {
|
||||
mockSupabaseSignIn.mockResolvedValue({
|
||||
data: { user: null, session: null },
|
||||
error: { message: "Invalid credentials" },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.loginUser("test@example.com", "Falsch!123", false);
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(result.current.errors.email).toBeDefined();
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ruft setUser und router.push bei erfolgreichem Login auf", async () => {
|
||||
const fakeUser = { id: "user-1", email: "test@example.com" };
|
||||
mockSupabaseSignIn.mockResolvedValue({
|
||||
data: { user: fakeUser, session: {} },
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.loginUser("test@example.com", "Richtig!123", false);
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
expect(mockSetUser).toHaveBeenCalledWith(fakeUser);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
|
||||
it("setzt loading auf false nach Abschluss", async () => {
|
||||
mockSupabaseSignIn.mockResolvedValue({
|
||||
data: { user: { id: "1" }, session: {} },
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loginUser("test@example.com", "Richtig!123", false);
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerUser", () => {
|
||||
const validFormData = {
|
||||
fullName: "Max Mustermann",
|
||||
email: "max@example.com",
|
||||
password: "Sicher!123",
|
||||
passwordConfirm: "Sicher!123",
|
||||
birthday: "1990-05-15",
|
||||
plz: "10115",
|
||||
place: "Berlin",
|
||||
street: "Musterstr. 1",
|
||||
gender: "Men" as const,
|
||||
};
|
||||
|
||||
it("gibt Zod-Fehler bei fehlenden Pflichtfeldern zurück", async () => {
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.registerUser({
|
||||
...validFormData,
|
||||
email: "keine-email",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(result.current.errors.email).toBeDefined();
|
||||
});
|
||||
|
||||
it("gibt true bei erfolgreicher Registrierung zurück", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ user: { id: "new-1" } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.registerUser(validFormData);
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
});
|
||||
|
||||
it("setzt Fehler bei API-Fehlerantwort", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: false,
|
||||
json: async () => ({ error: "Dieser Nutzer existiert bereits." }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAuth());
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.registerUser(validFormData);
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(result.current.errors.email).toContain("existiert bereits");
|
||||
});
|
||||
});
|
||||
});
|
||||
127
tests/hooks/useImageUpload.test.ts
Normal file
127
tests/hooks/useImageUpload.test.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useImageUpload } from "@/src/hooks/useImageUpload";
|
||||
|
||||
vi.mock("@/src/lib/supabase/storage", () => ({
|
||||
uploadFile: vi.fn(),
|
||||
deleteFile: vi.fn(),
|
||||
generateFilePath: vi.fn().mockReturnValue("users/user-1/1234567890.jpg"),
|
||||
}));
|
||||
|
||||
import { uploadFile, deleteFile } from "@/src/lib/supabase/storage";
|
||||
|
||||
const defaultOptions = { bucket: "avatars" as const };
|
||||
|
||||
function makeFile(name = "photo.jpg", type = "image/jpeg", sizeBytes = 1024): File {
|
||||
return new File([new Uint8Array(sizeBytes)], name, { type });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(uploadFile).mockReset();
|
||||
vi.mocked(deleteFile).mockReset();
|
||||
});
|
||||
|
||||
describe("useImageUpload", () => {
|
||||
it("gibt initialen State zurück", () => {
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
expect(result.current.isUploading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.progress).toBe(0);
|
||||
});
|
||||
|
||||
it("lehnt ungültigen MIME-Type ab und setzt error", async () => {
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
const pdfFile = new File(["pdf"], "doc.pdf", { type: "application/pdf" });
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.upload(pdfFile, "user-1")).rejects.toThrow();
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain("Ungültiger Dateityp");
|
||||
expect(result.current.isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it("lehnt Datei über maximaler Größe ab und setzt error", async () => {
|
||||
const { result } = renderHook(() => useImageUpload({ ...defaultOptions, maxSizeMB: 1 }));
|
||||
const bigFile = makeFile("big.jpg", "image/jpeg", 2 * 1024 * 1024);
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.upload(bigFile, "user-1")).rejects.toThrow();
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain("groß");
|
||||
expect(result.current.isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it("setzt isUploading auf false nach erfolgreichem Upload", async () => {
|
||||
vi.mocked(uploadFile).mockResolvedValue({
|
||||
path: "users/user-1/photo.jpg",
|
||||
publicUrl: "https://example.com/photo.jpg",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
const file = makeFile();
|
||||
|
||||
let url: string;
|
||||
await act(async () => {
|
||||
url = await result.current.upload(file, "user-1");
|
||||
});
|
||||
|
||||
expect(url!).toBe("https://example.com/photo.jpg");
|
||||
expect(result.current.isUploading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.progress).toBe(100);
|
||||
});
|
||||
|
||||
it("setzt error bei fehlgeschlagenem Upload", async () => {
|
||||
vi.mocked(uploadFile).mockRejectedValue(new Error("Netzwerkfehler"));
|
||||
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.upload(makeFile(), "user-1")).rejects.toThrow("Netzwerkfehler");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Netzwerkfehler");
|
||||
expect(result.current.isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it("ruft deleteFile mit korrektem Pfad auf", async () => {
|
||||
vi.mocked(deleteFile).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.remove("users/user-1/old-photo.jpg");
|
||||
});
|
||||
|
||||
expect(deleteFile).toHaveBeenCalledWith("avatars", "users/user-1/old-photo.jpg");
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it("setzt error wenn delete fehlschlägt", async () => {
|
||||
vi.mocked(deleteFile).mockRejectedValue(new Error("Löschen fehlgeschlagen"));
|
||||
|
||||
const { result } = renderHook(() => useImageUpload(defaultOptions));
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.remove("some/path.jpg")).rejects.toThrow();
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Löschen fehlgeschlagen");
|
||||
});
|
||||
|
||||
it("akzeptiert konfigurierbare erlaubte MIME-Types", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useImageUpload({ ...defaultOptions, allowedTypes: ["image/png"] })
|
||||
);
|
||||
|
||||
const jpegFile = makeFile("photo.jpg", "image/jpeg");
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.upload(jpegFile, "user-1")).rejects.toThrow();
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain("Ungültiger Dateityp");
|
||||
});
|
||||
});
|
||||
71
tests/hooks/useIsMobile.test.ts
Normal file
71
tests/hooks/useIsMobile.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useIsMobile } from "@/src/hooks/use-mobile";
|
||||
|
||||
const mockAddEventListener = vi.fn();
|
||||
const mockRemoveEventListener = vi.fn();
|
||||
|
||||
function setWindowWidth(width: number) {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: width,
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: (query: string) => ({
|
||||
matches: width < 768,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: mockAddEventListener,
|
||||
removeEventListener: mockRemoveEventListener,
|
||||
dispatchEvent: vi.fn(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockAddEventListener.mockClear();
|
||||
mockRemoveEventListener.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("useIsMobile", () => {
|
||||
it("gibt true zurück bei schmaler Viewport-Breite (<768px)", () => {
|
||||
setWindowWidth(500);
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("gibt false zurück bei breiter Viewport-Breite (>=768px)", () => {
|
||||
setWindowWidth(1024);
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("gibt false zurück genau bei 768px", () => {
|
||||
setWindowWidth(768);
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("registriert einen Event-Listener auf matchMedia", () => {
|
||||
setWindowWidth(500);
|
||||
renderHook(() => useIsMobile());
|
||||
expect(mockAddEventListener).toHaveBeenCalledWith("change", expect.any(Function));
|
||||
});
|
||||
|
||||
it("entfernt den Event-Listener beim Unmount", () => {
|
||||
setWindowWidth(500);
|
||||
const { unmount } = renderHook(() => useIsMobile());
|
||||
unmount();
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledWith("change", expect.any(Function));
|
||||
});
|
||||
});
|
||||
77
tests/msw/handlers.ts
Normal file
77
tests/msw/handlers.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { http, HttpResponse } from "msw";
|
||||
|
||||
export const handlers = [
|
||||
http.post("http://localhost/api/auth/login", async ({ request }) => {
|
||||
const body = (await request.json()) as Record<string, unknown>;
|
||||
|
||||
if (!body.email || !body.password) {
|
||||
return HttpResponse.json({ error: "E-Mail und Passwort sind erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.email === "wrong@example.com") {
|
||||
return HttpResponse.json({ error: "E-Mail oder Passwort ist falsch" }, { status: 401 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
user: { id: "user-1", email: body.email },
|
||||
session: { access_token: "token-123" },
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}),
|
||||
|
||||
http.post("http://localhost/api/auth/register", async ({ request }) => {
|
||||
const body = (await request.json()) as Record<string, unknown>;
|
||||
|
||||
if (!body.email || !body.password || !body.name) {
|
||||
return HttpResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.email === "existing@example.com") {
|
||||
return HttpResponse.json({ error: "Dieser Nutzer existiert bereits." }, { status: 409 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(
|
||||
{ user: { id: "new-user-1", email: body.email, name: body.name } },
|
||||
{ status: 201 }
|
||||
);
|
||||
}),
|
||||
|
||||
http.post("http://localhost/api/auth/register/employee", async ({ request }) => {
|
||||
const body = (await request.json()) as Record<string, unknown>;
|
||||
|
||||
if (!body.token || !body.email || !body.password || !body.firstName || !body.lastName) {
|
||||
return HttpResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.token === "expired-token") {
|
||||
return HttpResponse.json({ error: "Token abgelaufen" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (body.token === "invalid-token") {
|
||||
return HttpResponse.json({ error: "Ungültiger Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({ success: true }, { status: 201 });
|
||||
}),
|
||||
|
||||
http.post("http://localhost/api/upload/avatar", async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file");
|
||||
const token = formData.get("token");
|
||||
|
||||
if (!file || !token) {
|
||||
return HttpResponse.json({ error: "Datei und Token erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (token === "invalid-token") {
|
||||
return HttpResponse.json({ error: "Ungültiger Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(
|
||||
{ publicUrl: "https://example.com/avatars/employee-1/avatar.jpg" },
|
||||
{ status: 201 }
|
||||
);
|
||||
}),
|
||||
];
|
||||
4
tests/msw/server.ts
Normal file
4
tests/msw/server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { setupServer } from "msw/node";
|
||||
import { handlers } from "./handlers";
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
155
tests/schemas/courseSchema.test.ts
Normal file
155
tests/schemas/courseSchema.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { courseSchema, publishedCourseSchema } from "@/src/modules/courses/schemas/course-schema";
|
||||
|
||||
describe("courseSchema (Draft)", () => {
|
||||
it("akzeptiert komplett leere Daten (Entwurf)", () => {
|
||||
const result = courseSchema.safeParse({ isStandingOrder: false });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("akzeptiert leere String-Felder", () => {
|
||||
const result = courseSchema.safeParse({
|
||||
name: "",
|
||||
date: "",
|
||||
timeFrom: "",
|
||||
timeTo: "",
|
||||
isStandingOrder: false,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt timeFrom >= timeTo ab", () => {
|
||||
const result = courseSchema.safeParse({
|
||||
isStandingOrder: false,
|
||||
timeFrom: "14:00",
|
||||
timeTo: "10:00",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("timeTo");
|
||||
});
|
||||
|
||||
it("akzeptiert valide Zeitspanne", () => {
|
||||
const result = courseSchema.safeParse({
|
||||
isStandingOrder: false,
|
||||
timeFrom: "09:00",
|
||||
timeTo: "10:30",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const validPublishedCourse = {
|
||||
name: "Yoga Grundlagen",
|
||||
sport: ["Yoga"],
|
||||
level: "Anfänger",
|
||||
date: "2026-05-01",
|
||||
timeFrom: "09:00",
|
||||
timeTo: "10:30",
|
||||
trainers: ["trainer-1"],
|
||||
room: "raum-1",
|
||||
description: "Einführungskurs",
|
||||
maxParticipants: 15,
|
||||
price: 19.99,
|
||||
isStandingOrder: false,
|
||||
};
|
||||
|
||||
describe("publishedCourseSchema", () => {
|
||||
it("akzeptiert vollständig valide Kursdaten", () => {
|
||||
const result = publishedCourseSchema.safeParse(validPublishedCourse);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt fehlenden Kursnamen ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, name: "" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("name");
|
||||
});
|
||||
|
||||
it("lehnt leeres Sport-Array ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, sport: [] });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("sport");
|
||||
});
|
||||
|
||||
it("lehnt leeres Trainer-Array ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, trainers: [] });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("trainers");
|
||||
});
|
||||
|
||||
it("lehnt maxParticipants über 100 ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
maxParticipants: 101,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("maxParticipants");
|
||||
});
|
||||
|
||||
it("lehnt maxParticipants unter 1 ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, maxParticipants: 0 });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("maxParticipants");
|
||||
});
|
||||
|
||||
it("lehnt Preis über 9999.99 ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, price: 10000 });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("price");
|
||||
});
|
||||
|
||||
it("lehnt negativen Preis ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({ ...validPublishedCourse, price: -1 });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("price");
|
||||
});
|
||||
|
||||
it("lehnt timeFrom >= timeTo ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
timeFrom: "11:00",
|
||||
timeTo: "09:00",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("timeTo");
|
||||
});
|
||||
|
||||
it("lehnt Dauerauftrag ohne Häufigkeit ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
isStandingOrder: true,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("frequency");
|
||||
});
|
||||
|
||||
it("akzeptiert Dauerauftrag mit Häufigkeit", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
isStandingOrder: true,
|
||||
frequency: "weekly",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt individuelle Häufigkeit ohne Wochentage ab", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
isStandingOrder: true,
|
||||
frequency: "custom",
|
||||
weekdays: [],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("weekdays");
|
||||
});
|
||||
|
||||
it("akzeptiert individuelle Häufigkeit mit Wochentagen", () => {
|
||||
const result = publishedCourseSchema.safeParse({
|
||||
...validPublishedCourse,
|
||||
isStandingOrder: true,
|
||||
frequency: "custom",
|
||||
weekdays: ["Montag", "Mittwoch"],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
71
tests/schemas/employeeSchema.test.ts
Normal file
71
tests/schemas/employeeSchema.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { employeeSchema } from "@/src/modules/employee/schemas/employee-schema";
|
||||
|
||||
const defaultPermissions = {
|
||||
employees: { view: true, create: false, edit: false, delete: false },
|
||||
courses: { view: true, create: false, edit: false, delete: false },
|
||||
rooms: { view: true, create: false, edit: false, delete: false },
|
||||
};
|
||||
|
||||
const validData = {
|
||||
email: "mitarbeiter@example.com",
|
||||
roles: ["Trainer"],
|
||||
isMultipleLocation: false,
|
||||
permissions: defaultPermissions,
|
||||
};
|
||||
|
||||
describe("employeeSchema", () => {
|
||||
it("akzeptiert valide Mitarbeiterdaten", () => {
|
||||
const result = employeeSchema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt ungültige E-Mail ab", () => {
|
||||
const result = employeeSchema.safeParse({ ...validData, email: "keine-email" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("email");
|
||||
});
|
||||
|
||||
it("lehnt leeres Rollen-Array ab", () => {
|
||||
const result = employeeSchema.safeParse({ ...validData, roles: [] });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("roles");
|
||||
});
|
||||
|
||||
it("lehnt mehrere Standorte ohne Standort-Auswahl ab", () => {
|
||||
const result = employeeSchema.safeParse({
|
||||
...validData,
|
||||
isMultipleLocation: true,
|
||||
locations: [],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("locations");
|
||||
});
|
||||
|
||||
it("akzeptiert mehrere Standorte mit mindestens einem Standort", () => {
|
||||
const result = employeeSchema.safeParse({
|
||||
...validData,
|
||||
isMultipleLocation: true,
|
||||
locations: ["standort-1"],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("akzeptiert mehrere Rollen", () => {
|
||||
const result = employeeSchema.safeParse({
|
||||
...validData,
|
||||
roles: ["Trainer", "Admin", "Kassierer"],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("akzeptiert alle Permissions als false", () => {
|
||||
const noPermissions = {
|
||||
employees: { view: false, create: false, edit: false, delete: false },
|
||||
courses: { view: false, create: false, edit: false, delete: false },
|
||||
rooms: { view: false, create: false, edit: false, delete: false },
|
||||
};
|
||||
const result = employeeSchema.safeParse({ ...validData, permissions: noPermissions });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
40
tests/schemas/userLoginSchema.test.ts
Normal file
40
tests/schemas/userLoginSchema.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { userLoginSchema } from "@/src/modules/auth/validation/userLoginSchema";
|
||||
|
||||
describe("userLoginSchema", () => {
|
||||
it("akzeptiert valide Anmeldedaten", () => {
|
||||
const result = userLoginSchema.safeParse({
|
||||
email: "test@example.com",
|
||||
password: "passwort123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt ungültige E-Mail ab", () => {
|
||||
const result = userLoginSchema.safeParse({
|
||||
email: "kein-email",
|
||||
password: "passwort123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("email");
|
||||
});
|
||||
|
||||
it("lehnt Passwort unter 8 Zeichen ab", () => {
|
||||
const result = userLoginSchema.safeParse({
|
||||
email: "test@example.com",
|
||||
password: "kurz",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("password");
|
||||
});
|
||||
|
||||
it("lehnt fehlende E-Mail ab", () => {
|
||||
const result = userLoginSchema.safeParse({ password: "passwort123" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("lehnt fehlendes Passwort ab", () => {
|
||||
const result = userLoginSchema.safeParse({ email: "test@example.com" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
100
tests/schemas/userRegisterSchema.test.ts
Normal file
100
tests/schemas/userRegisterSchema.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { userRegisterSchema } from "@/src/modules/auth/validation/userRegisterSchema";
|
||||
|
||||
const validData = {
|
||||
fullName: "Max Mustermann",
|
||||
email: "max@example.com",
|
||||
password: "Sicher!123",
|
||||
passwordConfirm: "Sicher!123",
|
||||
birthday: "1990-05-15",
|
||||
plz: "10115",
|
||||
place: "Berlin",
|
||||
street: "Musterstraße 1",
|
||||
gender: "Men" as const,
|
||||
};
|
||||
|
||||
describe("userRegisterSchema", () => {
|
||||
it("akzeptiert vollständig valide Daten", () => {
|
||||
const result = userRegisterSchema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("lehnt zu kurzen Namen ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, fullName: "X" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("fullName");
|
||||
});
|
||||
|
||||
it("lehnt ungültige E-Mail ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, email: "keine-email" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("email");
|
||||
});
|
||||
|
||||
it("lehnt Passwort ohne Großbuchstaben ab", () => {
|
||||
const result = userRegisterSchema.safeParse({
|
||||
...validData,
|
||||
password: "sicher!123",
|
||||
passwordConfirm: "sicher!123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("password");
|
||||
});
|
||||
|
||||
it("lehnt Passwort ohne Sonderzeichen ab", () => {
|
||||
const result = userRegisterSchema.safeParse({
|
||||
...validData,
|
||||
password: "Sicher1234",
|
||||
passwordConfirm: "Sicher1234",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("password");
|
||||
});
|
||||
|
||||
it("lehnt Passwort unter 8 Zeichen ab", () => {
|
||||
const result = userRegisterSchema.safeParse({
|
||||
...validData,
|
||||
password: "Ku!1",
|
||||
passwordConfirm: "Ku!1",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("lehnt nicht übereinstimmende Passwörter ab", () => {
|
||||
const result = userRegisterSchema.safeParse({
|
||||
...validData,
|
||||
passwordConfirm: "AndersPasswort!1",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("passwordConfirm");
|
||||
});
|
||||
|
||||
it("lehnt Geburtsdatum in der Zukunft ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, birthday: "2099-01-01" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("birthday");
|
||||
});
|
||||
|
||||
it("lehnt ungültiges Datumsformat ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, birthday: "15.05.1990" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("lehnt fehlendes Geschlecht ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, gender: "" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe("gender");
|
||||
});
|
||||
|
||||
it("lehnt ungültigen Gender-Wert ab", () => {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, gender: "Unknown" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("akzeptiert alle Gender-Werte", () => {
|
||||
for (const gender of ["Men", "Woman", "Various"] as const) {
|
||||
const result = userRegisterSchema.safeParse({ ...validData, gender });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
37
tests/utils/utils.test.ts
Normal file
37
tests/utils/utils.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { cn } from "@/src/lib/utils";
|
||||
|
||||
describe("cn", () => {
|
||||
it("gibt eine einzelne Klasse zurück", () => {
|
||||
expect(cn("px-4")).toBe("px-4");
|
||||
});
|
||||
|
||||
it("kombiniert mehrere Klassen", () => {
|
||||
expect(cn("px-4", "py-2", "text-sm")).toBe("px-4 py-2 text-sm");
|
||||
});
|
||||
|
||||
it("überschreibt konfliktive Tailwind-Klassen (twMerge)", () => {
|
||||
expect(cn("px-2", "px-4")).toBe("px-4");
|
||||
expect(cn("text-sm", "text-lg")).toBe("text-lg");
|
||||
});
|
||||
|
||||
it("ignoriert falsy-Werte (clsx)", () => {
|
||||
expect(cn("px-4", false && "text-red", undefined, null as never, "py-2")).toBe("px-4 py-2");
|
||||
});
|
||||
|
||||
it("verarbeitet bedingte Objekte", () => {
|
||||
expect(cn({ "px-4": true, "py-2": false, "text-sm": true })).toBe("px-4 text-sm");
|
||||
});
|
||||
|
||||
it("verarbeitet Arrays", () => {
|
||||
expect(cn(["px-4", "py-2"])).toBe("px-4 py-2");
|
||||
});
|
||||
|
||||
it("gibt leeren String bei keinen Argumenten zurück", () => {
|
||||
expect(cn()).toBe("");
|
||||
});
|
||||
|
||||
it("gibt leeren String bei nur falsy-Werten zurück", () => {
|
||||
expect(cn(false, undefined, null as never)).toBe("");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./vitest.setup.ts",
|
||||
include: ["**/*.test.{ts,tsx}"],
|
||||
include: ["tests/**/*.test.{ts,tsx}"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "lcov"],
|
||||
include: [
|
||||
"src/app/api/**",
|
||||
"src/hooks/**",
|
||||
"src/modules/**/schemas/**",
|
||||
"src/modules/**/validation/**",
|
||||
"src/lib/**",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
import "@testing-library/jest-dom";
|
||||
import { server } from "./tests/msw/server";
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue