mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-29 19:29:37 +02:00
258 lines
8.1 KiB
TypeScript
258 lines
8.1 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { handle } from "./hooks.server";
|
|
import { mockCookies } from "$lib/tests/const";
|
|
|
|
// Mock the sequence function to avoid the request store issue
|
|
vi.mock("@sveltejs/kit/hooks", () => ({
|
|
sequence: (...handlers: any[]) => {
|
|
// Return a simple sequential handler that doesn't require request store
|
|
return async ({ event, resolve }: any) => {
|
|
let currentResolve = resolve;
|
|
|
|
// Chain handlers in reverse order
|
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
const handler = handlers[i];
|
|
const previousResolve = currentResolve;
|
|
currentResolve = async (event: any) => {
|
|
return handler({ event, resolve: previousResolve });
|
|
};
|
|
}
|
|
|
|
return currentResolve(event);
|
|
};
|
|
},
|
|
}));
|
|
|
|
// Mock the startup service
|
|
vi.mock("$lib/server/services/startup-service", () => ({
|
|
StartupService: {
|
|
initialize: vi.fn(() => Promise.resolve()),
|
|
},
|
|
}));
|
|
|
|
// Mock auth services
|
|
vi.mock("$lib/server/auth/session-service", () => ({
|
|
SessionService: {
|
|
validateSession: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("$lib/server/auth/jwt-utils", () => ({
|
|
verifyAccessToken: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("$lib/server/auth/authorization-service", () => ({
|
|
AuthorizationService: {
|
|
hasRole: vi.fn(),
|
|
hasAnyRole: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock the Date.now function for rate limiting tests
|
|
const mockDateNow = vi.fn();
|
|
const OriginalDate = Date;
|
|
vi.stubGlobal(
|
|
"Date",
|
|
class extends OriginalDate {
|
|
static now = mockDateNow;
|
|
},
|
|
);
|
|
|
|
describe("hooks.server", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockDateNow.mockReturnValue(1000000); // Fixed timestamp for consistent testing
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Reset any global state
|
|
vi.clearAllTimers();
|
|
});
|
|
|
|
describe("rate limiting", () => {
|
|
const mockResolve = vi.fn();
|
|
const createEvent = (ip: string = "192.168.1.1", method: string = "GET") => {
|
|
const request = new Request("http://localhost/api/health", {
|
|
method,
|
|
headers: {
|
|
"x-forwarded-for": ip,
|
|
},
|
|
});
|
|
|
|
return {
|
|
url: new URL("http://localhost/api/health"),
|
|
request,
|
|
cookies: mockCookies as any,
|
|
fetch: {} as any,
|
|
getClientAddress: () => ip,
|
|
locals: {},
|
|
params: {},
|
|
route: { id: null },
|
|
setHeaders: vi.fn(),
|
|
isDataRequest: false,
|
|
isSubRequest: false,
|
|
platform: {} as any,
|
|
tracing: { enabled: false, root: "", current: "" },
|
|
isRemoteRequest: false,
|
|
};
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockResolve.mockResolvedValue(new Response("OK"));
|
|
});
|
|
|
|
it("should allow requests within rate limit", async () => {
|
|
const event = createEvent("192.168.1.100"); // Unique IP for this test
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.status).not.toBe(429);
|
|
expect(mockResolve).toHaveBeenCalled();
|
|
expect(mockResolve.mock.calls[0][0]).toEqual(event);
|
|
});
|
|
|
|
it("should block requests when rate limit exceeded", async () => {
|
|
const event = createEvent("192.168.1.101"); // Unique IP for this test
|
|
|
|
// Make multiple requests to exceed rate limit
|
|
for (let i = 0; i < 10; i++) {
|
|
await handle({ event, resolve: mockResolve });
|
|
}
|
|
|
|
// This request should be rate limited
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.status).toBe(429);
|
|
expect(await response.text()).toBe("Too Many Requests");
|
|
});
|
|
|
|
it("should reset rate limit after window expires", async () => {
|
|
const event = createEvent("192.168.1.102"); // Unique IP for this test
|
|
|
|
// Exceed rate limit
|
|
for (let i = 0; i < 11; i++) {
|
|
await handle({ event, resolve: mockResolve });
|
|
}
|
|
|
|
// Mock time passing (would need to mock Date.now in real implementation)
|
|
// For now, we'll test that different IPs are treated separately
|
|
const differentIPEvent = createEvent("192.168.1.2");
|
|
const response = await handle({ event: differentIPEvent, resolve: mockResolve });
|
|
|
|
expect(response.status).not.toBe(429);
|
|
});
|
|
});
|
|
|
|
describe("CORS handling", () => {
|
|
const mockResolve = vi.fn();
|
|
|
|
const createCORSEvent = (method: string = "GET", path: string = "/api/health") => ({
|
|
url: new URL(`http://localhost${path}`),
|
|
request: new Request(`http://localhost${path}`, {
|
|
method,
|
|
headers: {
|
|
"x-forwarded-for": "192.168.2.1", // Different IP for CORS tests
|
|
},
|
|
}),
|
|
cookies: mockCookies as any,
|
|
fetch: {} as any,
|
|
getClientAddress: () => "192.168.2.1",
|
|
locals: {},
|
|
params: {},
|
|
route: { id: null },
|
|
setHeaders: vi.fn(),
|
|
isDataRequest: false,
|
|
isSubRequest: false,
|
|
platform: {} as any,
|
|
tracing: { enabled: false, root: "", current: "" },
|
|
isRemoteRequest: false,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mockResolve.mockResolvedValue(new Response("OK"));
|
|
});
|
|
|
|
it("should handle OPTIONS preflight requests", async () => {
|
|
const event = createCORSEvent("OPTIONS");
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
|
|
expect(mockResolve).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should add CORS headers to API routes", async () => {
|
|
const event = createCORSEvent();
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
|
|
});
|
|
});
|
|
|
|
describe("security headers", () => {
|
|
const mockResolve = vi.fn();
|
|
|
|
const createSecurityEvent = (protocol: string = "http", path: string = "/test") => ({
|
|
url: new URL(`${protocol}://localhost${path}`),
|
|
request: new Request(`${protocol}://localhost${path}`, {
|
|
headers: {
|
|
"x-forwarded-for": "192.168.3.1", // Different IP for security tests
|
|
},
|
|
}),
|
|
cookies: mockCookies as any,
|
|
fetch: {} as any,
|
|
getClientAddress: () => "192.168.3.1",
|
|
locals: {},
|
|
params: {},
|
|
route: { id: null },
|
|
setHeaders: vi.fn(),
|
|
isDataRequest: false,
|
|
isSubRequest: false,
|
|
platform: {} as any,
|
|
tracing: { enabled: false, root: "", current: "" },
|
|
isRemoteRequest: false,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mockResolve.mockResolvedValue(new Response("OK"));
|
|
});
|
|
|
|
it("should add security headers to all responses", async () => {
|
|
const event = createSecurityEvent();
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.headers.get("X-Frame-Options")).toBe("DENY");
|
|
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
expect(response.headers.get("X-XSS-Protection")).toBe("1; mode=block");
|
|
expect(response.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
|
|
expect(response.headers.get("Content-Security-Policy")).toContain(
|
|
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://unpkg.com; style-src 'self' 'unsafe-inline' https://unpkg.com; font-src 'self' data: https://unpkg.com; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests",
|
|
);
|
|
});
|
|
|
|
it("should add HSTS header for HTTPS requests", async () => {
|
|
const event = createSecurityEvent("https");
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.headers.get("Strict-Transport-Security")).toBe(
|
|
"max-age=31536000; includeSubDomains; preload",
|
|
);
|
|
});
|
|
|
|
it("should not add HSTS header for HTTP requests", async () => {
|
|
const event = createSecurityEvent("http");
|
|
|
|
const response = await handle({ event, resolve: mockResolve });
|
|
|
|
expect(response.headers.get("Strict-Transport-Security")).toBeNull();
|
|
});
|
|
});
|
|
});
|