forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlockout.test.ts
More file actions
90 lines (76 loc) · 3.03 KB
/
Copy pathlockout.test.ts
File metadata and controls
90 lines (76 loc) · 3.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// src/lib/__tests__/lockout.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest"
vi.mock("@/lib/db", () => ({
db: {
update: vi.fn(),
},
}))
vi.mock("@/lib/db/schema", () => ({
appSettings: {},
}))
import { db } from "@/lib/db"
import { checkLockout, recordFailedAttempt } from "@/lib/lockout"
const mockUpdate = db.update as ReturnType<typeof vi.fn>
describe("checkLockout", () => {
const baseLockout = { lockoutEnabled: true, lockoutThreshold: 5, lockoutDurationMinutes: 15 }
it("returns null when not locked", () => {
expect(checkLockout({ ...baseLockout, lockedUntil: null })).toBeNull()
})
it("returns null when lock expired", () => {
const past = new Date(Date.now() - 60_000)
expect(checkLockout({ ...baseLockout, lockedUntil: past })).toBeNull()
})
it("returns 429 response when locked", () => {
const future = new Date(Date.now() + 60_000)
const result = checkLockout({ ...baseLockout, lockedUntil: future })
expect(result).not.toBeNull()
expect(result?.status).toBe(429)
})
it("returns null when lockout is disabled even if lockedUntil is set", () => {
const future = new Date(Date.now() + 60_000)
expect(checkLockout({ ...baseLockout, lockoutEnabled: false, lockedUntil: future })).toBeNull()
})
})
describe("recordFailedAttempt", () => {
const lockoutSettings = { lockoutEnabled: true, lockoutThreshold: 5, lockoutDurationMinutes: 15 }
function setupMockChain(failedLoginAttempts: number) {
const chain = {
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([{ failedLoginAttempts }]),
}
mockUpdate.mockReturnValue(chain)
return chain
}
beforeEach(() => {
vi.clearAllMocks()
})
it("increments the counter via atomic SQL", async () => {
const chain = setupMockChain(1)
await recordFailedAttempt(1, lockoutSettings)
expect(mockUpdate).toHaveBeenCalled()
expect(chain.set).toHaveBeenCalled()
expect(chain.returning).toHaveBeenCalled()
// The argument to .set() must contain a failedLoginAttempts key built from a
// drizzle sql`` expression — i.e. not a plain numeric literal — so that the
// increment is atomic at the database level and avoids read-modify-write races.
const setArg = chain.set.mock.calls[0][0] as Record<string, unknown>
expect(setArg).toHaveProperty("failedLoginAttempts")
expect(typeof setArg.failedLoginAttempts).not.toBe("number")
})
it("does not set lockout below threshold", async () => {
setupMockChain(3)
await recordFailedAttempt(1, lockoutSettings)
expect(mockUpdate).toHaveBeenCalledTimes(1)
})
it("sets lockout when attempts reach threshold", async () => {
setupMockChain(5)
await recordFailedAttempt(1, lockoutSettings)
expect(mockUpdate).toHaveBeenCalledTimes(2)
})
it("does not set lockout when disabled", async () => {
setupMockChain(100)
await recordFailedAttempt(1, { ...lockoutSettings, lockoutEnabled: false })
expect(mockUpdate).toHaveBeenCalledTimes(1)
})
})