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

192 lines
6.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { forgetTenantTrustedDevice, getTenantSecuritySnapshot, revokeTenantUserSession, TenantSecurityError, type TenantSecurityDeps } from "./tenant-security.server";
import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
function makeDb() {
return {
tenant: {
findUnique: vi.fn(async () => ({
id: "tenant_1",
name: "Acme",
slug: "acme",
active: true,
users: [
{
status: 1,
user: {
id: "user_1",
email: "owner@example.test",
firstName: "Owner",
lastName: "Example",
active: true,
totpEnabledAt: new Date("2026-07-01T00:00:00.000Z"),
totpPendingCreatedAt: null,
mfaRecoveryCodeHashes: [{ hash: "x" }],
},
},
],
})),
},
userSession: {
findMany: vi.fn(async () => [
{
id: "session_1",
userId: "user_1",
deviceLabel: "MacBook",
userAgent: "Safari",
approxLocation: "DE",
createdAt: new Date("2026-07-09T08:00:00.000Z"),
lastSeenAt: new Date("2026-07-09T09:00:00.000Z"),
expiresAt: new Date("2026-07-10T09:00:00.000Z"),
revokedAt: null,
trustedAt: null,
user: { email: "owner@example.test" },
},
]),
findFirst: vi.fn(async () => ({ id: "session_1", revokedAt: null, userId: "user_1", user: { email: "owner@example.test" } })),
update: vi.fn(async () => ({})),
},
trustedDevice: {
findMany: vi.fn(async () => [
{
id: "device_1",
userId: "user_1",
deviceLabel: "Safari",
approxLocation: "DE",
lastUsedAt: new Date("2026-07-09T08:00:00.000Z"),
expiresAt: new Date("2026-08-09T08:00:00.000Z"),
revokedAt: null,
user: { email: "owner@example.test" },
},
]),
findFirst: vi.fn(async () => ({ id: "device_1", revokedAt: null, userId: "user_1", user: { email: "owner@example.test" } })),
update: vi.fn(async () => ({})),
},
magicLinkToken: {
findMany: vi.fn(async () => []),
},
tenantIpAddress: {
findMany: vi.fn(async () => [
{
id: "ip_1",
ip: "192.168.10.25",
fromUrl: "/login",
createdAt: new Date("2026-07-09T08:00:00.000Z"),
user: { email: "owner@example.test" },
apiKey: null,
},
]),
},
};
}
function makeDeps(db = makeDb()) {
return {
db,
now: () => new Date("2026-07-09T10:00:00.000Z"),
listAuditEvents: vi.fn(async () => ({
rows: [
{
id: "audit_1",
createdAt: new Date("2026-07-09T09:00:00.000Z"),
action: "auth.login.success",
outcome: "success",
actorEmail: "owner@example.test",
actorId: "user_1",
summaryText: "Login erfolgreich",
},
],
total: 1,
take: 25,
skip: 0,
})),
recordAdminAction: vi.fn(async () => {}),
} as unknown as TenantSecurityDeps & { db: ReturnType<typeof makeDb>; recordAdminAction: ReturnType<typeof vi.fn>; listAuditEvents: ReturnType<typeof vi.fn> };
}
describe("getTenantSecuritySnapshot", () => {
it("loads security diagnostics for one tenant", async () => {
const deps = makeDeps();
deps.db.tenant.findUnique.mockImplementationOnce(async () => {
expect(getRlsExecutionContext()?.tenantId).toBe("tenant_1");
return makeDb().tenant.findUnique();
});
const snapshot = await getTenantSecuritySnapshot("tenant_1", deps);
expect(deps.db.tenant.findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "tenant_1" } }));
expect(deps.db.userSession.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { userId: { in: ["user_1"] } } }));
expect(deps.listAuditEvents).toHaveBeenCalledWith(expect.objectContaining({ take: 25 }));
expect(snapshot.users[0]).toMatchObject({ email: "owner@example.test", mfaEnabled: true, recoveryCodesConfigured: true });
expect(snapshot.sessions[0]).toMatchObject({ id: "session_1", userEmail: "owner@example.test" });
expect(snapshot.ipLogs[0]).toMatchObject({ ip: "192.168.10.25" });
});
it("raises a typed error when the tenant does not exist", async () => {
const db = makeDb();
db.tenant.findUnique.mockResolvedValueOnce(null);
await expect(getTenantSecuritySnapshot("missing", makeDeps(db))).rejects.toMatchObject({ code: "TENANT_NOT_FOUND" });
});
});
describe("tenant security operator actions", () => {
it("revokes a session and records an admin audit event", async () => {
const deps = makeDeps();
deps.db.userSession.update.mockImplementationOnce(async () => {
expect(getRlsExecutionContext()?.tenantId).toBe("tenant_1");
return {};
});
await revokeTenantUserSession(
{
tenantId: "tenant_1",
sessionId: "session_1",
reason: "Supportfall MFA Reset",
actorUserId: "admin_1",
actorEmail: "admin@hivegrid.de",
},
deps
);
expect(deps.db.userSession.update).toHaveBeenCalledWith({
where: { id: "session_1" },
data: { revokedAt: new Date("2026-07-09T10:00:00.000Z") },
});
expect(deps.recordAdminAction).toHaveBeenCalledWith(expect.objectContaining({ action: "security.sessionRevoked", targetId: "session_1" }));
});
it("forgets a trusted device and records an admin audit event", async () => {
const deps = makeDeps();
deps.db.trustedDevice.update.mockImplementationOnce(async () => {
expect(getRlsExecutionContext()?.tenantId).toBe("tenant_1");
return {};
});
await forgetTenantTrustedDevice(
{
tenantId: "tenant_1",
trustedDeviceId: "device_1",
reason: "Geraet verloren",
actorUserId: "admin_1",
},
deps
);
expect(deps.db.trustedDevice.update).toHaveBeenCalledWith({
where: { id: "device_1" },
data: { revokedAt: new Date("2026-07-09T10:00:00.000Z") },
});
expect(deps.recordAdminAction).toHaveBeenCalledWith(expect.objectContaining({ action: "security.trustedDeviceForgotten", targetId: "device_1" }));
});
it("returns a typed error for unknown sessions", async () => {
const db = makeDb();
db.userSession.findFirst.mockResolvedValueOnce(null);
await expect(
revokeTenantUserSession({ tenantId: "tenant_1", sessionId: "missing", reason: "Supportfall MFA Reset", actorUserId: "admin_1" }, makeDeps(db))
).rejects.toBeInstanceOf(TenantSecurityError);
});
});