fix: remove nested .git folders, re-add as normal directories
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { mockApi, mockOptima, mockIsInvalidSignatureError, mockRedirect } =
|
||||
vi.hoisted(() => ({
|
||||
mockApi: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
mockOptima: {
|
||||
auth: {},
|
||||
user: {
|
||||
isLoggedIn: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
refreshSession: vi.fn(),
|
||||
fetchInfo: vi.fn(),
|
||||
checkPermissions: vi.fn(),
|
||||
},
|
||||
},
|
||||
mockIsInvalidSignatureError: vi.fn(),
|
||||
mockRedirect: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("$lib", () => ({ optima: mockOptima }));
|
||||
vi.mock("$lib/optima-api/axios", () => ({ default: mockApi, api: mockApi }));
|
||||
vi.mock("$lib/optima-api/errorHandler", () => ({
|
||||
isInvalidSignatureError: mockIsInvalidSignatureError,
|
||||
}));
|
||||
vi.mock("@sveltejs/kit", () => ({
|
||||
redirect: mockRedirect,
|
||||
}));
|
||||
|
||||
import { handle } from "./hooks.server";
|
||||
|
||||
function createMockEvent(overrides: Record<string, unknown> = {}) {
|
||||
const cookies: Record<string, string> = {};
|
||||
return {
|
||||
url: new URL("http://localhost/"),
|
||||
cookies: {
|
||||
get: vi.fn((name: string) => cookies[name] ?? null),
|
||||
set: vi.fn((name: string, value: string) => {
|
||||
cookies[name] = value;
|
||||
}),
|
||||
delete: vi.fn((name: string) => {
|
||||
delete cookies[name];
|
||||
}),
|
||||
},
|
||||
locals: {} as Record<string, unknown>,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createResolve(response = new Response("OK")) {
|
||||
return vi.fn().mockResolvedValue(response);
|
||||
}
|
||||
|
||||
describe("hooks.server handle", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockIsInvalidSignatureError.mockReturnValue(false);
|
||||
});
|
||||
|
||||
// ── Health check bypass ───────────────────────────────────────────
|
||||
|
||||
it("passes /healthz through without API check", async () => {
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/healthz"),
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(mockApi.get).not.toHaveBeenCalled();
|
||||
expect(resolve).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
// ── API health check ─────────────────────────────────────────────
|
||||
|
||||
it("returns 503 page when API teapot check fails", async () => {
|
||||
mockApi.get.mockRejectedValueOnce(new Error("Network error"));
|
||||
const event = createMockEvent();
|
||||
const resolve = createResolve();
|
||||
|
||||
const response = await handle({ event, resolve } as any);
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
const html = await response.text();
|
||||
expect(html).toContain("Unable to Reach API");
|
||||
});
|
||||
|
||||
it("returns 503 when API teapot returns non-418 status", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 200 });
|
||||
const event = createMockEvent();
|
||||
const resolve = createResolve();
|
||||
|
||||
const response = await handle({ event, resolve } as any);
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
});
|
||||
|
||||
// ── Logout path ──────────────────────────────────────────────────
|
||||
|
||||
it("clears cookies and redirects on /logout", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/logout"),
|
||||
});
|
||||
event.cookies.get = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce("access-tok")
|
||||
.mockReturnValueOnce("refresh-tok");
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(event.cookies.delete).toHaveBeenCalledWith("accessToken", {
|
||||
path: "/",
|
||||
});
|
||||
expect(event.cookies.delete).toHaveBeenCalledWith("refreshToken", {
|
||||
path: "/",
|
||||
});
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
|
||||
});
|
||||
|
||||
// ── Login path when already logged in ─────────────────────────────
|
||||
|
||||
it("redirects to / when visiting /login while already logged in", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockOptima.user.isLoggedIn.mockReturnValue(true);
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/login"),
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/");
|
||||
});
|
||||
|
||||
// ── Login path when not logged in ─────────────────────────────────
|
||||
|
||||
it("resolves /login normally when user is not logged in", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockOptima.user.isLoggedIn.mockReturnValue(false);
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/login"),
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(resolve).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
// ── No tokens — force logout ──────────────────────────────────────
|
||||
|
||||
it("forces logout when no access or refresh tokens exist", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn().mockReturnValue(null);
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(mockOptima.user.logout).toHaveBeenCalled();
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
|
||||
});
|
||||
|
||||
// ── Valid access token — normal flow ──────────────────────────────
|
||||
|
||||
it("resolves normally with valid non-expired access token", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
// Create a JWT payload with exp far in the future
|
||||
const payload = {
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
sub: "user-1",
|
||||
};
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
|
||||
"base64url",
|
||||
);
|
||||
const fakeJwt = `header.${encodedPayload}.signature`;
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return fakeJwt;
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(resolve).toHaveBeenCalledWith(event);
|
||||
// setTokens should have set session on locals
|
||||
expect(event.locals.session).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Expired access token — refresh ────────────────────────────────
|
||||
|
||||
it("refreshes expired access token using refresh token", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
// Create an expired JWT
|
||||
const payload = { exp: Math.floor(Date.now() / 1000) - 100, sub: "u1" };
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
|
||||
"base64url",
|
||||
);
|
||||
const fakeJwt = `h.${encodedPayload}.s`;
|
||||
|
||||
mockOptima.user.refreshSession.mockResolvedValueOnce({
|
||||
accessToken: "new-access",
|
||||
refreshToken: "new-refresh",
|
||||
});
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return fakeJwt;
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
|
||||
expect(event.cookies.set).toHaveBeenCalledWith(
|
||||
"accessToken",
|
||||
"new-access",
|
||||
{ path: "/" },
|
||||
);
|
||||
expect(resolve).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
// ── Expired token, no refresh token — force logout ────────────────
|
||||
|
||||
it("forces logout when token expired and no refresh token", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
const payload = { exp: Math.floor(Date.now() / 1000) - 100, sub: "u1" };
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
|
||||
"base64url",
|
||||
);
|
||||
const fakeJwt = `h.${encodedPayload}.s`;
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return fakeJwt;
|
||||
if (name === "refreshToken") return null;
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(mockOptima.user.logout).toHaveBeenCalled();
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
|
||||
});
|
||||
|
||||
// ── Invalid signature error — force logout ────────────────────────
|
||||
|
||||
it("forces logout on invalid signature error during token decode", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockIsInvalidSignatureError.mockReturnValue(true);
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
// Return a malformed JWT that will throw when parsed
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return "bad.!!!.token";
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(mockOptima.user.logout).toHaveBeenCalled();
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
|
||||
});
|
||||
|
||||
// ── Malformed token, refresh succeeds ─────────────────────────────
|
||||
|
||||
it("refreshes when access token is malformed and refresh token exists", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockIsInvalidSignatureError.mockReturnValue(false);
|
||||
mockOptima.user.refreshSession.mockResolvedValueOnce({
|
||||
accessToken: "recovered-access",
|
||||
refreshToken: "recovered-refresh",
|
||||
});
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return "totally.broken";
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
|
||||
expect(resolve).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ── No access token, has refresh — try refresh ────────────────────
|
||||
|
||||
it("attempts refresh when only refresh token exists", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockOptima.user.refreshSession.mockResolvedValueOnce({
|
||||
accessToken: "new-access",
|
||||
refreshToken: "new-refresh",
|
||||
});
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return null;
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
await handle({ event, resolve } as any);
|
||||
|
||||
expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
|
||||
expect(resolve).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ── No access token, refresh fails — force logout ─────────────────
|
||||
|
||||
it("forces logout when only refresh token exists but refresh fails", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
mockIsInvalidSignatureError.mockReturnValue(false);
|
||||
mockOptima.user.refreshSession.mockRejectedValueOnce(
|
||||
new Error("Refresh failed"),
|
||||
);
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return null;
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(mockOptima.user.logout).toHaveBeenCalled();
|
||||
expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
|
||||
});
|
||||
|
||||
// ── Refresh fails with invalid signature ──────────────────────────
|
||||
|
||||
it("logs warning on invalid signature during refresh fallback", async () => {
|
||||
mockApi.get.mockResolvedValueOnce({ status: 418 });
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
// First call: malformed token parse error (not invalid signature)
|
||||
// Second call: refresh also fails with invalid signature
|
||||
mockIsInvalidSignatureError
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(true);
|
||||
mockOptima.user.refreshSession.mockRejectedValueOnce(
|
||||
new Error("invalid signature"),
|
||||
);
|
||||
|
||||
const event = createMockEvent({
|
||||
url: new URL("http://localhost/dashboard"),
|
||||
});
|
||||
event.cookies.get = vi.fn((name: string) => {
|
||||
if (name === "accessToken") return "bad.!!!.token";
|
||||
if (name === "refreshToken") return "refresh-tok";
|
||||
return null;
|
||||
});
|
||||
const resolve = createResolve();
|
||||
|
||||
try {
|
||||
await handle({ event, resolve } as any);
|
||||
} catch {
|
||||
// redirect throws
|
||||
}
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
"Invalid refresh token signature — forcing logout.",
|
||||
);
|
||||
expect(mockOptima.user.logout).toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user