fix(tests): align 20 drifted tests with current source behavior

Tests fell behind source changes: lastTotpAt replay-attack prevention,
activeSession invalidation on password reset, select clauses in
permission updates, UNAUTHORIZED (anti-enumeration) for disabled TOTP,
and password minimum raised from 8 to 12 characters.

Also fix root eslint.config.mjs to ignore packages/ (linted via turbo)
and add --no-warn-ignored to lint-staged to suppress warnings for
ignored files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:41:42 +02:00
parent 9bd3781c03
commit dfeb4d361e
11 changed files with 711 additions and 545 deletions
+53 -27
View File
@@ -81,10 +81,12 @@ describe("user.linkResource", () => {
},
});
await expect(caller.linkResource({
userId: "missing_user",
resourceId: "resource_1",
})).rejects.toMatchObject({
await expect(
caller.linkResource({
userId: "missing_user",
resourceId: "resource_1",
}),
).rejects.toMatchObject({
code: "NOT_FOUND",
message: "User not found",
});
@@ -112,10 +114,12 @@ describe("user.linkResource", () => {
},
});
await expect(caller.linkResource({
userId: "user_1",
resourceId: "missing_resource",
})).rejects.toMatchObject({
await expect(
caller.linkResource({
userId: "user_1",
resourceId: "missing_resource",
}),
).rejects.toMatchObject({
code: "NOT_FOUND",
message: "Resource not found",
});
@@ -141,10 +145,12 @@ describe("user.linkResource", () => {
},
});
await expect(caller.linkResource({
userId: "user_1",
resourceId: "resource_1",
})).rejects.toMatchObject({
await expect(
caller.linkResource({
userId: "user_1",
resourceId: "resource_1",
}),
).rejects.toMatchObject({
code: "CONFLICT",
message: "Resource is already linked to another user",
});
@@ -155,7 +161,8 @@ describe("user.linkResource", () => {
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()
const updateMany = vi
.fn()
.mockResolvedValueOnce({ count: 1 })
.mockResolvedValueOnce({ count: 1 });
const caller = createAdminCaller({
@@ -224,7 +231,8 @@ describe("user.linkResource", () => {
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()
const updateMany = vi
.fn()
.mockResolvedValueOnce({ count: 0 })
.mockResolvedValueOnce({ count: 0 });
const caller = createAdminCaller({
@@ -237,10 +245,12 @@ describe("user.linkResource", () => {
},
});
await expect(caller.linkResource({
userId: "user_1",
resourceId: "resource_1",
})).rejects.toMatchObject({
await expect(
caller.linkResource({
userId: "user_1",
resourceId: "resource_1",
}),
).rejects.toMatchObject({
code: "CONFLICT",
message: "Resource link changed during update. Please retry.",
});
@@ -249,7 +259,9 @@ describe("user.linkResource", () => {
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 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: {
@@ -279,24 +291,29 @@ describe("user admin account management", () => {
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: "secret123",
password: "SecurePass123!",
});
expect(result).toEqual({ success: true });
expect(argon2HashMock).toHaveBeenCalledWith("secret123");
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 () => {
@@ -436,6 +453,7 @@ describe("user permission overrides", () => {
expect(update).toHaveBeenCalledWith({
where: { id: "user_2" },
data: { permissionOverrides: overrides },
select: { id: true, name: true, email: true, permissionOverrides: true },
});
});
@@ -472,6 +490,7 @@ describe("user permission overrides", () => {
expect(update).toHaveBeenCalledWith({
where: { id: "user_2" },
data: { permissionOverrides: Prisma.DbNull },
select: { id: true, name: true, email: true, permissionOverrides: true },
});
});
@@ -709,7 +728,7 @@ describe("user profile and TOTP self-service", () => {
expect(result).toEqual({ enabled: true });
expect(update).toHaveBeenCalledWith({
where: { id: "user_admin" },
data: { totpEnabled: true },
data: { totpEnabled: true, lastTotpAt: expect.any(Date) },
});
});
@@ -721,10 +740,13 @@ describe("user profile and TOTP self-service", () => {
id: "user_admin",
totpSecret: "MOCKSECRET",
totpEnabled: true,
lastTotpAt: null,
});
const update = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
user: {
findUnique,
update,
},
});
@@ -733,7 +755,11 @@ describe("user profile and TOTP self-service", () => {
expect(result).toEqual({ valid: true });
expect(findUnique).toHaveBeenCalledWith({
where: { id: "user_admin" },
select: { id: true, totpSecret: true, totpEnabled: true },
select: { id: true, totpSecret: true, totpEnabled: true, lastTotpAt: true },
});
expect(update).toHaveBeenCalledWith({
where: { id: "user_admin" },
data: { lastTotpAt: expect.any(Date) },
});
});
@@ -752,7 +778,9 @@ describe("user profile and TOTP self-service", () => {
},
});
await expect(caller.verifyTotp({ userId: "user_admin", token: "123456" })).rejects.toMatchObject({
await expect(
caller.verifyTotp({ userId: "user_admin", token: "123456" }),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Invalid TOTP token.",
});
@@ -763,9 +791,7 @@ 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 } },
],
widgets: [{ id: "peakTimes", position: { x: 0, y: 0, w: 4, h: 3 } }],
},
updatedAt: new Date("2026-03-30T18:00:00.000Z"),
});