4ff7bc90c3
Expand the SSRF blocklist from IPv4-only to IPv6 loopback/ULA (fc00::/7)/
link-local (fe80::/10)/multicast/IPv4-mapped, plus the missing IPv4 ranges
0.0.0.0/8, 100.64.0.0/10 CGNAT, and TEST-NET/benchmark ranges. Replace the
single-lookup SSRF guard with resolveAndValidate(): resolves all DNS records
(lookup { all: true }) so a hostname returning "public + private" is
rejected, and returns the first validated address for connection pinning.
The webhook dispatcher now switches from plain fetch() to https.request()
with a custom Agent.lookup that returns the pre-validated IP. A DNS rebind
between the guard check and the TCP connect() can no longer redirect the
dial to an internal address. Hostname still flows through for SNI and
certificate validation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1055 lines
28 KiB
TypeScript
1055 lines
28 KiB
TypeScript
import { Prisma } from "@capakraken/db";
|
|
import { resolvePermissions, SystemRole } from "@capakraken/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { userRouter } from "../router/user.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
const { totpValidateMock } = vi.hoisted(() => ({
|
|
totpValidateMock: vi.fn(),
|
|
}));
|
|
|
|
const { argon2HashMock } = vi.hoisted(() => ({
|
|
argon2HashMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../lib/audit.js", () => ({
|
|
createAuditEntry: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock("@node-rs/argon2", () => ({
|
|
hash: argon2HashMock,
|
|
}));
|
|
|
|
vi.mock("otpauth", () => {
|
|
class Secret {
|
|
base32: string;
|
|
|
|
constructor() {
|
|
this.base32 = "MOCKSECRET";
|
|
}
|
|
|
|
static fromBase32(value: string) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
class TOTP {
|
|
validate(args: { token: string; window: number }) {
|
|
return totpValidateMock(args);
|
|
}
|
|
|
|
toString() {
|
|
return "otpauth://mock";
|
|
}
|
|
}
|
|
|
|
return { Secret, TOTP };
|
|
});
|
|
|
|
const createCaller = createCallerFactory(userRouter);
|
|
|
|
function createAdminCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "admin@example.com", name: "Admin", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_admin",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
},
|
|
roleDefaults: null,
|
|
});
|
|
}
|
|
|
|
describe("user.linkResource", () => {
|
|
it("returns NOT_FOUND when the target user does not exist", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue(null);
|
|
const resourceFindUnique = vi.fn();
|
|
const updateMany = vi.fn();
|
|
const update = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
update,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
caller.linkResource({
|
|
userId: "missing_user",
|
|
resourceId: "resource_1",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
code: "NOT_FOUND",
|
|
message: "User not found",
|
|
});
|
|
|
|
expect(userFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "missing_user" },
|
|
select: { id: true },
|
|
});
|
|
expect(resourceFindUnique).not.toHaveBeenCalled();
|
|
expect(updateMany).not.toHaveBeenCalled();
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns NOT_FOUND when the target resource does not exist", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
|
const resourceFindUnique = vi.fn().mockResolvedValue(null);
|
|
const updateMany = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
caller.linkResource({
|
|
userId: "user_1",
|
|
resourceId: "missing_resource",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
code: "NOT_FOUND",
|
|
message: "Resource not found",
|
|
});
|
|
|
|
expect(resourceFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "missing_resource" },
|
|
select: { id: true, userId: true },
|
|
});
|
|
expect(updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns CONFLICT when the requested resource is already linked to another user", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
|
const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: "user_2" });
|
|
const updateMany = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
caller.linkResource({
|
|
userId: "user_1",
|
|
resourceId: "resource_1",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
code: "CONFLICT",
|
|
message: "Resource is already linked to another user",
|
|
});
|
|
|
|
expect(updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("unlinks existing assignments before linking the requested resource", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
|
const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: null });
|
|
const updateMany = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ count: 1 })
|
|
.mockResolvedValueOnce({ count: 1 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
const result = await caller.linkResource({
|
|
userId: "user_1",
|
|
resourceId: "resource_1",
|
|
});
|
|
|
|
expect(result).toEqual({ success: true });
|
|
expect(updateMany).toHaveBeenNthCalledWith(1, {
|
|
where: {
|
|
userId: "user_1",
|
|
NOT: { id: "resource_1" },
|
|
},
|
|
data: { userId: null },
|
|
});
|
|
expect(updateMany).toHaveBeenNthCalledWith(2, {
|
|
where: {
|
|
id: "resource_1",
|
|
OR: [{ userId: null }, { userId: "user_1" }],
|
|
},
|
|
data: { userId: "user_1" },
|
|
});
|
|
});
|
|
|
|
it("unlinks a user without checking or updating a replacement resource", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
|
const resourceFindUnique = vi.fn();
|
|
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
const result = await caller.linkResource({
|
|
userId: "user_1",
|
|
resourceId: null,
|
|
});
|
|
|
|
expect(result).toEqual({ success: true });
|
|
expect(userFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_1" },
|
|
select: { id: true },
|
|
});
|
|
expect(resourceFindUnique).not.toHaveBeenCalled();
|
|
expect(updateMany).toHaveBeenCalledWith({
|
|
where: { userId: "user_1" },
|
|
data: { userId: null },
|
|
});
|
|
});
|
|
|
|
it("returns CONFLICT when the resource link changes between validation and update", async () => {
|
|
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
|
const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: null });
|
|
const updateMany = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ count: 0 })
|
|
.mockResolvedValueOnce({ count: 0 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique: userFindUnique,
|
|
},
|
|
resource: {
|
|
findUnique: resourceFindUnique,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
caller.linkResource({
|
|
userId: "user_1",
|
|
resourceId: "resource_1",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
code: "CONFLICT",
|
|
message: "Resource link changed during update. Please retry.",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user admin account management", () => {
|
|
it("counts users active within the last five minutes", async () => {
|
|
const nowSpy = vi
|
|
.spyOn(Date, "now")
|
|
.mockReturnValue(new Date("2026-03-30T20:00:00.000Z").valueOf());
|
|
const count = vi.fn().mockResolvedValue(4);
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
count,
|
|
},
|
|
});
|
|
|
|
const result = await caller.activeCount();
|
|
|
|
expect(result).toEqual({ count: 4 });
|
|
expect(count).toHaveBeenCalledWith({
|
|
where: {
|
|
lastActiveAt: { gte: new Date("2026-03-30T19:55:00.000Z") },
|
|
},
|
|
});
|
|
|
|
nowSpy.mockRestore();
|
|
});
|
|
|
|
it("hashes and stores a replacement password for an existing user", async () => {
|
|
argon2HashMock.mockReset();
|
|
argon2HashMock.mockResolvedValue("hashed-secret");
|
|
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_2",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const deleteMany = vi.fn().mockResolvedValue({ count: 0 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
activeSession: {
|
|
deleteMany,
|
|
},
|
|
});
|
|
|
|
const result = await caller.setPassword({
|
|
userId: "user_2",
|
|
password: "SecurePass123!",
|
|
});
|
|
|
|
expect(result).toEqual({ success: true });
|
|
expect(argon2HashMock).toHaveBeenCalledWith("SecurePass123!");
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
data: { passwordHash: "hashed-secret" },
|
|
});
|
|
expect(deleteMany).toHaveBeenCalledWith({ where: { userId: "user_2" } });
|
|
});
|
|
|
|
it("updates the selected user's display name", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_2",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
});
|
|
const update = vi.fn().mockResolvedValue({
|
|
id: "user_2",
|
|
name: "Alice Updated",
|
|
email: "alice@example.com",
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.updateName({
|
|
id: "user_2",
|
|
name: "Alice Updated",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
id: "user_2",
|
|
name: "Alice Updated",
|
|
email: "alice@example.com",
|
|
});
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
select: { id: true, name: true, email: true },
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
data: { name: "Alice Updated" },
|
|
select: { id: true, name: true, email: true },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user.autoLinkAllByEmail", () => {
|
|
it("links only users with an unclaimed resource that matches their email", async () => {
|
|
const userFindMany = vi.fn().mockResolvedValue([
|
|
{ id: "user_1", email: "alice@example.com" },
|
|
{ id: "user_2", email: "bob@example.com" },
|
|
{ id: "user_3", email: "carol@example.com" },
|
|
]);
|
|
const resourceFindFirst = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ id: "resource_1" })
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({ id: "resource_3" });
|
|
const resourceUpdate = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findMany: userFindMany,
|
|
},
|
|
resource: {
|
|
findFirst: resourceFindFirst,
|
|
update: resourceUpdate,
|
|
},
|
|
});
|
|
|
|
const result = await caller.autoLinkAllByEmail();
|
|
|
|
expect(result).toEqual({ linked: 2, checked: 3 });
|
|
expect(userFindMany).toHaveBeenCalledWith({
|
|
where: { resource: null },
|
|
select: { id: true, email: true },
|
|
});
|
|
expect(resourceFindFirst).toHaveBeenNthCalledWith(1, {
|
|
where: { email: "alice@example.com", userId: null },
|
|
select: { id: true },
|
|
});
|
|
expect(resourceFindFirst).toHaveBeenNthCalledWith(2, {
|
|
where: { email: "bob@example.com", userId: null },
|
|
select: { id: true },
|
|
});
|
|
expect(resourceFindFirst).toHaveBeenNthCalledWith(3, {
|
|
where: { email: "carol@example.com", userId: null },
|
|
select: { id: true },
|
|
});
|
|
expect(resourceUpdate).toHaveBeenCalledTimes(2);
|
|
expect(resourceUpdate).toHaveBeenNthCalledWith(1, {
|
|
where: { id: "resource_1" },
|
|
data: { userId: "user_1" },
|
|
});
|
|
expect(resourceUpdate).toHaveBeenNthCalledWith(2, {
|
|
where: { id: "resource_3" },
|
|
data: { userId: "user_3" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user permission overrides", () => {
|
|
it("stores explicit permission overrides for a user", async () => {
|
|
const before = {
|
|
id: "user_2",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
permissionOverrides: null,
|
|
};
|
|
const updated = {
|
|
id: "user_2",
|
|
permissionOverrides: {
|
|
granted: ["manageProjects"],
|
|
denied: ["viewCosts"],
|
|
chapterIds: ["chapter_design"],
|
|
},
|
|
};
|
|
const findUnique = vi.fn().mockResolvedValue(before);
|
|
const update = vi.fn().mockResolvedValue(updated);
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const overrides = {
|
|
granted: ["manageProjects"],
|
|
denied: ["viewCosts"],
|
|
chapterIds: ["chapter_design"],
|
|
};
|
|
const result = await caller.setPermissions({
|
|
userId: "user_2",
|
|
overrides,
|
|
});
|
|
|
|
expect(result).toEqual(updated);
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
select: { id: true, name: true, email: true, permissionOverrides: true },
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
data: { permissionOverrides: overrides },
|
|
select: { id: true, name: true, email: true, permissionOverrides: true },
|
|
});
|
|
});
|
|
|
|
it("resets permission overrides back to role defaults", async () => {
|
|
const before = {
|
|
id: "user_2",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
permissionOverrides: {
|
|
granted: ["manageProjects"],
|
|
denied: ["viewCosts"],
|
|
},
|
|
};
|
|
const updated = {
|
|
id: "user_2",
|
|
permissionOverrides: null,
|
|
};
|
|
const findUnique = vi.fn().mockResolvedValue(before);
|
|
const update = vi.fn().mockResolvedValue(updated);
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.resetPermissions({ userId: "user_2" });
|
|
|
|
expect(result).toEqual(updated);
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
select: { id: true, name: true, email: true, permissionOverrides: true },
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
data: { permissionOverrides: Prisma.DbNull },
|
|
select: { id: true, name: true, email: true, permissionOverrides: true },
|
|
});
|
|
});
|
|
|
|
it("returns resolved effective permissions together with stored overrides", async () => {
|
|
const overrides = {
|
|
granted: ["manageProjects"],
|
|
denied: ["viewCosts"],
|
|
chapterIds: ["chapter_design"],
|
|
};
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
systemRole: SystemRole.MANAGER,
|
|
permissionOverrides: overrides,
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.getEffectivePermissions({ userId: "user_2" });
|
|
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_2" },
|
|
select: { systemRole: true, permissionOverrides: true },
|
|
});
|
|
expect(result).toEqual({
|
|
systemRole: SystemRole.MANAGER,
|
|
effectivePermissions: Array.from(resolvePermissions(SystemRole.MANAGER, overrides)),
|
|
overrides,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user profile and TOTP self-service", () => {
|
|
it("returns the authenticated user profile by db user id", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
createdAt: new Date("2026-03-30T08:00:00.000Z"),
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.me();
|
|
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
systemRole: true,
|
|
permissionOverrides: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
createdAt: new Date("2026-03-30T08:00:00.000Z"),
|
|
});
|
|
});
|
|
|
|
it("uses the db user id for self-service profile reads even when the session email is stale", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
createdAt: new Date("2026-03-30T08:00:00.000Z"),
|
|
});
|
|
const caller = createCaller({
|
|
session: {
|
|
user: { email: "stale@example.com", name: "Admin", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: {
|
|
user: {
|
|
findUnique,
|
|
},
|
|
} as never,
|
|
dbUser: {
|
|
id: "user_admin",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
},
|
|
roleDefaults: null,
|
|
});
|
|
|
|
await caller.me();
|
|
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
systemRole: true,
|
|
permissionOverrides: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("stores a new TOTP secret for the current user and returns the enrollment URI", async () => {
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.generateTotpSecret();
|
|
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: { totpSecret: "MOCKSECRET" },
|
|
});
|
|
expect(result).toEqual({
|
|
secret: "MOCKSECRET",
|
|
uri: "otpauth://mock",
|
|
});
|
|
});
|
|
|
|
it("rejects enabling TOTP when no secret was generated", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
totpSecret: null,
|
|
totpEnabled: false,
|
|
});
|
|
const update = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({
|
|
code: "BAD_REQUEST",
|
|
message: "No TOTP secret generated. Call generateTotpSecret first.",
|
|
});
|
|
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects enabling TOTP when it is already active", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
totpSecret: "MOCKSECRET",
|
|
totpEnabled: true,
|
|
});
|
|
const update = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({
|
|
code: "BAD_REQUEST",
|
|
message: "TOTP is already enabled.",
|
|
});
|
|
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects invalid TOTP tokens during self-service enablement", async () => {
|
|
totpValidateMock.mockReset();
|
|
totpValidateMock.mockReturnValue(null);
|
|
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
totpSecret: "MOCKSECRET",
|
|
totpEnabled: false,
|
|
});
|
|
const update = vi.fn();
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({
|
|
code: "BAD_REQUEST",
|
|
message: "Invalid TOTP token.",
|
|
});
|
|
|
|
expect(totpValidateMock).toHaveBeenCalledWith({
|
|
token: "123456",
|
|
window: 1,
|
|
});
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("enables TOTP for the current user when the token validates", async () => {
|
|
totpValidateMock.mockReset();
|
|
totpValidateMock.mockReturnValue(0);
|
|
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
totpSecret: "MOCKSECRET",
|
|
totpEnabled: false,
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
const result = await caller.verifyAndEnableTotp({ token: "123456" });
|
|
|
|
expect(result).toEqual({ enabled: true });
|
|
// lastTotpAt is written atomically by updateMany (the replay guard);
|
|
// user.update only toggles the enabled flag after the CAS succeeds.
|
|
expect(updateMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({ data: { lastTotpAt: expect.any(Date) } }),
|
|
);
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: { totpEnabled: true },
|
|
});
|
|
});
|
|
|
|
it("verifies login-flow TOTP tokens successfully when enabled", async () => {
|
|
totpValidateMock.mockReset();
|
|
totpValidateMock.mockReturnValue(0);
|
|
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
totpSecret: "MOCKSECRET",
|
|
totpEnabled: true,
|
|
lastTotpAt: null,
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
updateMany,
|
|
},
|
|
});
|
|
|
|
const result = await caller.verifyTotp({ userId: "user_admin", token: "123456" });
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: { id: true, totpSecret: true, totpEnabled: true, lastTotpAt: true },
|
|
});
|
|
expect(updateMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({ data: { lastTotpAt: expect.any(Date) } }),
|
|
);
|
|
});
|
|
|
|
it("rejects invalid login-flow TOTP tokens with UNAUTHORIZED", async () => {
|
|
totpValidateMock.mockReset();
|
|
totpValidateMock.mockReturnValue(null);
|
|
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "user_admin",
|
|
totpSecret: "MOCKSECRET",
|
|
totpEnabled: true,
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
caller.verifyTotp({ userId: "user_admin", token: "123456" }),
|
|
).rejects.toMatchObject({
|
|
code: "UNAUTHORIZED",
|
|
message: "Invalid TOTP token.",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user dashboard and favorites", () => {
|
|
it("returns null layout when stored data has no valid widget types (bug #27 guard)", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
dashboardLayout: {
|
|
widgets: [{ id: "peakTimes", position: { x: 0, y: 0, w: 4, h: 3 } }],
|
|
},
|
|
updatedAt: new Date("2026-03-30T18:00:00.000Z"),
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.getDashboardLayout();
|
|
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: { dashboardLayout: true, updatedAt: true },
|
|
});
|
|
// Widgets with unknown types are dropped → empty → client uses default layout
|
|
expect(result).toEqual({
|
|
layout: null,
|
|
updatedAt: new Date("2026-03-30T18:00:00.000Z"),
|
|
});
|
|
});
|
|
|
|
it("persists a valid dashboard layout for the current user", async () => {
|
|
const update = vi.fn().mockResolvedValue({
|
|
updatedAt: new Date("2026-03-30T19:00:00.000Z"),
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
update,
|
|
},
|
|
});
|
|
|
|
const layout = {
|
|
version: 2,
|
|
gridCols: 16,
|
|
widgets: [],
|
|
};
|
|
|
|
const result = await caller.saveDashboardLayout({ layout });
|
|
|
|
expect(result).toEqual({
|
|
updatedAt: new Date("2026-03-30T19:00:00.000Z"),
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: { dashboardLayout: layout },
|
|
select: { updatedAt: true },
|
|
});
|
|
});
|
|
|
|
it("persists dashboard layout by db user id even when the session email is stale", async () => {
|
|
const update = vi.fn().mockResolvedValue({
|
|
updatedAt: new Date("2026-03-30T19:30:00.000Z"),
|
|
});
|
|
const caller = createCaller({
|
|
session: {
|
|
user: { email: "stale@example.com", name: "Admin", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: {
|
|
user: {
|
|
update,
|
|
},
|
|
} as never,
|
|
dbUser: {
|
|
id: "user_admin",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
},
|
|
roleDefaults: null,
|
|
});
|
|
|
|
await caller.saveDashboardLayout({
|
|
layout: {
|
|
version: 2,
|
|
gridCols: 16,
|
|
widgets: [],
|
|
},
|
|
});
|
|
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: {
|
|
dashboardLayout: {
|
|
version: 2,
|
|
gridCols: 16,
|
|
widgets: [],
|
|
},
|
|
},
|
|
select: { updatedAt: true },
|
|
});
|
|
});
|
|
|
|
it("returns favorite project ids as an empty list when none are stored", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
favoriteProjectIds: null,
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.getFavoriteProjectIds();
|
|
|
|
expect(result).toEqual([]);
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: { favoriteProjectIds: true },
|
|
});
|
|
});
|
|
|
|
it("adds a project to favorites when it is not already present", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
favoriteProjectIds: ["project_1"],
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.toggleFavoriteProject({ projectId: "project_2" });
|
|
|
|
expect(result).toEqual({
|
|
favoriteProjectIds: ["project_1", "project_2"],
|
|
added: true,
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: { favoriteProjectIds: ["project_1", "project_2"] },
|
|
});
|
|
});
|
|
|
|
it("removes a project from favorites when it is already present", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
favoriteProjectIds: ["project_1", "project_2"],
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.toggleFavoriteProject({ projectId: "project_2" });
|
|
|
|
expect(result).toEqual({
|
|
favoriteProjectIds: ["project_1"],
|
|
added: false,
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: { favoriteProjectIds: ["project_1"] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("user column preferences and MFA status", () => {
|
|
it("returns empty column preferences when nothing is stored", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
columnPreferences: null,
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.getColumnPreferences();
|
|
|
|
expect(result).toEqual({});
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: { columnPreferences: true },
|
|
});
|
|
});
|
|
|
|
it("merges column preferences without dropping untouched fields", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
columnPreferences: {
|
|
resources: {
|
|
visible: ["name", "role"],
|
|
sort: { field: "name", dir: "asc" },
|
|
rowOrder: ["name", "role", "email"],
|
|
},
|
|
},
|
|
});
|
|
const update = vi.fn().mockResolvedValue({ id: "user_admin" });
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.setColumnPreferences({
|
|
view: "resources",
|
|
visible: ["name", "email"],
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
data: {
|
|
columnPreferences: {
|
|
resources: {
|
|
visible: ["name", "email"],
|
|
sort: { field: "name", dir: "asc" },
|
|
rowOrder: ["name", "role", "email"],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reports the current MFA enabled state", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
totpEnabled: true,
|
|
});
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.getMfaStatus();
|
|
|
|
expect(result).toEqual({ totpEnabled: true });
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "user_admin" },
|
|
select: { totpEnabled: true },
|
|
});
|
|
});
|
|
|
|
it("returns NOT_FOUND for MFA status when the current user record no longer exists", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const caller = createAdminCaller({
|
|
user: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
await expect(caller.getMfaStatus()).rejects.toMatchObject({
|
|
code: "NOT_FOUND",
|
|
message: "User not found",
|
|
});
|
|
});
|
|
});
|