Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/services/auth/sessionLifecycle.server.test.ts
T

139 lines
5.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const storeMock = vi.hoisted(() => ({
createUserSession: vi.fn(),
revokeSessionByToken: vi.fn(),
}));
const adminStoreMock = vi.hoisted(() => ({
createAdminSession: vi.fn(),
revokeAdminSessionByToken: vi.fn(),
}));
vi.mock("@hivegrid/config", async (importOriginal) => {
const actual = await importOriginal<typeof import("@hivegrid/config")>();
return {
...actual,
loadRequiredWebConfig: () => ({
routing: {
appBaseDomain: "example.test",
protocol: "https",
webAppHost: "app.example.test",
adminAppHost: "admin.example.test",
},
}),
};
});
vi.mock("@hivegrid/db/admin-sessions.server", () => ({
createAdminSession: (...args: unknown[]) => adminStoreMock.createAdminSession(...args),
revokeAdminSessionByToken: (...args: unknown[]) => adminStoreMock.revokeAdminSessionByToken(...args),
}));
vi.mock("./sessionStore.server", () => ({
createUserSession: (...args: unknown[]) => storeMock.createUserSession(...args),
revokeSessionByToken: (...args: unknown[]) => storeMock.revokeSessionByToken(...args),
}));
const geoMock = vi.hoisted(() => ({ lookupCountry: vi.fn() }));
vi.mock("~/modules/ipAddress/services/geoip.server", () => ({
// Realistischer Mock: der Hash enthaelt die Roh-IP NICHT (wie der echte hashIp).
hashIp: () => "HASHED",
lookupCountry: (...args: unknown[]) => geoMock.lookupCountry(...args),
}));
import { buildSessionDeviceContext, endServerSession, startServerSession } from "./sessionLifecycle.server";
function requestWith(headers: Record<string, string>) {
return new Request("https://example.test/login", { headers });
}
function adminRequestWith(headers: Record<string, string>) {
return new Request("https://admin.example.test/login", { headers });
}
beforeEach(() => {
vi.clearAllMocks();
geoMock.lookupCountry.mockResolvedValue({ countryCode: "DE", countryName: "Germany" });
});
describe("buildSessionDeviceContext (AD-22)", () => {
it("hashes the IP and resolves the country, never exposing the raw IP", async () => {
const ctx = await buildSessionDeviceContext(
requestWith({
"user-agent": "Safari",
"cf-connecting-ip": "9.9.9.9",
"x-forwarded-for": "198.51.100.9, 173.245.48.10",
}),
);
expect(ctx.ipHash).toBe("HASHED");
expect(ctx.userAgent).toBe("Safari");
expect(ctx.approxLocation).toBe("Germany");
expect(JSON.stringify(ctx)).not.toContain("9.9.9.9");
});
it("yields nulls and skips the geo lookup when no IP is present", async () => {
const ctx = await buildSessionDeviceContext(requestWith({}));
expect(ctx.ipHash).toBeNull();
expect(ctx.userAgent).toBeNull();
expect(ctx.approxLocation).toBeNull();
expect(geoMock.lookupCountry).not.toHaveBeenCalled();
});
});
describe("startServerSession", () => {
it("creates a session row with the pseudonymized device context and returns the raw token", async () => {
storeMock.createUserSession.mockResolvedValue({ session: { id: "s1" }, rawToken: "tok-123" });
const token = await startServerSession({
request: requestWith({
"user-agent": "Safari",
"cf-connecting-ip": "1.2.3.4",
"x-forwarded-for": "198.51.100.4, 173.245.48.10",
}),
userId: "u1",
});
expect(token).toBe("tok-123");
expect(storeMock.createUserSession).toHaveBeenCalledWith({ userId: "u1", ipHash: "HASHED", userAgent: "Safari", approxLocation: "Germany" });
});
it("creates a dedicated eight-hour AdminSession only with Authentik MFA assurance", async () => {
const mfaAssuredAt = new Date("2026-07-24T18:00:00.000Z");
adminStoreMock.createAdminSession.mockResolvedValue({ session: { id: "as1" }, rawToken: "admin-token" });
const token = await startServerSession({
request: adminRequestWith({ "user-agent": "Safari" }),
userId: "admin-1",
adminAuthenticationMethods: ["pwd", "mfa"],
adminMfaAssuredAt: mfaAssuredAt,
});
expect(token).toBe("admin-token");
expect(adminStoreMock.createAdminSession).toHaveBeenCalledWith({
adminUserId: "admin-1",
authenticationMethods: ["pwd", "mfa"],
mfaAssuredAt,
ipHash: null,
userAgent: "Safari",
approxLocation: null,
});
expect(storeMock.createUserSession).not.toHaveBeenCalled();
});
it("fails closed on the admin host without MFA assurance", async () => {
await expect(startServerSession({ request: adminRequestWith({}), userId: "admin-1" })).rejects.toThrow(
"Admin server session requires Authentik MFA assurance"
);
expect(adminStoreMock.createAdminSession).not.toHaveBeenCalled();
});
});
describe("endServerSession", () => {
it("delegates revocation to the store", async () => {
storeMock.revokeSessionByToken.mockResolvedValue(true);
await endServerSession("tok-123");
expect(storeMock.revokeSessionByToken).toHaveBeenCalledWith("tok-123");
});
it("revokes only the dedicated AdminSession on the admin host", async () => {
adminStoreMock.revokeAdminSessionByToken.mockResolvedValue(true);
const request = adminRequestWith({});
await endServerSession("admin-token", request);
expect(adminStoreMock.revokeAdminSessionByToken).toHaveBeenCalledWith("admin-token");
expect(storeMock.revokeSessionByToken).not.toHaveBeenCalled();
});
});