api/tests: make the queue and avatar doubles observable
The queue mock handed out a fresh throwaway per lookup, so specs could not see what got scheduled, and the avatar mock never set the uploaded file, forcing every upload test through a 400. Both now record what they are asked to do, and the S3 client is replaced so deletion stays offline
Pedro Lucas Porcellis porcellis@eletrotupi.com · 29 days ago
bfd15c20b7ee35a4fc80ceb63a3973266258e876 Browse filesParents
7c99110Changed files
3
Additions
+125
Deletions
-26
/dev/null → api/tests/mocks/avatar.ts
1 +
import { vi } from 'vitest'
2 +
3 +
// multer-s3 (and the S3 client) is replaced by this middleware in tests, so
4 +
// the avatar specs can stage the file the controller will read off req.file
5 +
let uploadedFile: unknown = null
6 +
7 +
export function setUploadedFile(file: unknown) {
8 +
uploadedFile = file
9 +
}
10 +
11 +
export function resetAvatarMock() {
12 +
uploadedFile = null
13 +
14 +
avatarMulterMock.single.mockClear()
15 +
}
16 +
17 +
export const avatarMulterMock = {
18 +
single: vi.fn(
19 +
() => (req: any, _res: any, next: any) => {
20 +
if (uploadedFile) {
21 +
req.file = uploadedFile
22 +
}
23 +
24 +
next()
25 +
},
26 +
),
27 +
}
/dev/null → api/tests/mocks/queue.ts
1 +
import { vi } from 'vitest'
2 +
3 +
export type RecordedJob = {
4 +
queue: string
5 +
name: string
6 +
data: unknown
7 +
options?: unknown
8 +
}
9 +
10 +
// Every job enqueued through a mocked queue lands here, so specs can assert
11 +
// on what the controllers/services scheduled without a real Valkey
12 +
export const recordedJobs: RecordedJob[] = []
13 +
14 +
function createQueueMock(name: string) {
15 +
return {
16 +
name,
17 +
18 +
add: vi.fn(async (jobName: string, data: unknown, options?: unknown) => {
19 +
recordedJobs.push({ queue: name, name: jobName, data, options })
20 +
21 +
return {}
22 +
}),
23 +
24 +
addBulk: vi.fn(async (jobs: { name: string; data: unknown; opts?: unknown }[] = []) => {
25 +
for (const job of jobs) {
26 +
recordedJobs.push({
27 +
queue: name,
28 +
name: job.name,
29 +
data: job.data,
30 +
options: job.opts
31 +
})
32 +
}
33 +
return []
34 +
}),
35 +
36 +
getRepeatableJobs: vi.fn(async () => []),
37 +
removeRepeatableByKey: vi.fn(async () => undefined),
38 +
}
39 +
}
40 +
41 +
const queues = new Map<string, ReturnType<typeof createQueueMock>>()
42 +
43 +
export function getQueueMock(name: string) {
44 +
if (!queues.has(name)) {
45 +
queues.set(name, createQueueMock(name))
46 +
}
47 +
48 +
return queues.get(name)!
49 +
}
50 +
51 +
export function resetQueueMocks() {
52 +
recordedJobs.length = 0
53 +
54 +
for (const queue of queues.values()) {
55 +
queue.add.mockClear()
56 +
queue.addBulk.mockClear()
57 +
queue.getRepeatableJobs.mockClear()
58 +
queue.removeRepeatableByKey.mockClear()
59 +
}
60 +
}
api/tests/setup.ts
1 1
import { vi } from 'vitest'
2 2
3 -
const mockQueue = () => ({
4 -
add: vi.fn().mockResolvedValue({}),
5 -
addBulk: vi.fn().mockResolvedValue([]),
6 -
getRepeatableJobs: vi.fn().mockResolvedValue([]),
7 -
removeRepeatableByKey: vi.fn().mockResolvedValue(undefined),
8 -
})
3 +
// Mock the barrel export used by controllers and most services. `getQueue`
4 +
// returns a stable per-name mock so specs can assert on scheduled jobs via
5 +
// `recordedJobs` from ./mocks/queue
6 +
vi.mock('@app/lib/queue', async () => {
7 +
const { getQueueMock } = await import('./mocks/queue')
9 8
10 -
// Mock the barrel export used by controllers and most services
11 -
vi.mock('@app/lib/queue', () => ({
12 -
bootWorkers: vi.fn(),
13 -
closeAllWorkers: vi.fn(),
14 -
closeAllQueues: vi.fn(),
15 -
scheduleInsights: vi.fn(),
16 -
getQueue: vi.fn(() => mockQueue()),
17 -
MailJobName: {
18 -
WelcomeEmail: 'mail:welcome',
19 -
PasswordReset: 'mail:password-reset',
20 -
ActivateAccountEmail: 'mail:activate-account',
21 -
},
22 -
}))
9 +
return {
10 +
bootWorkers: vi.fn(),
11 +
closeAllWorkers: vi.fn(),
12 +
closeAllQueues: vi.fn(),
13 +
scheduleInsights: vi.fn(),
14 +
getQueue: (name: string) => getQueueMock(name),
15 +
MailJobName: {
16 +
WelcomeEmail: 'mail:welcome',
17 +
PasswordReset: 'mail:password-reset',
18 +
ActivateAccountEmail: 'mail:activate-account',
19 +
},
20 +
}
21 +
})
23 22
24 23
// syncMedicineReminderJobs imports getQueue directly from QueueRegistry
25 -
vi.mock('@app/lib/queue/QueueRegistry', () => ({
26 -
getQueue: vi.fn(() => mockQueue()),
27 -
closeAllQueues: vi.fn(),
28 -
}))
24 +
vi.mock('@app/lib/queue/QueueRegistry', async () => {
25 +
const { getQueueMock } = await import('./mocks/queue')
29 26
30 -
// avatar.ts constructs multer-s3 at import time and requires S3_BUCKET env var
31 -
vi.mock('@app/services/avatar', () => ({
32 -
default: { single: vi.fn(() => (_req: any, _res: any, next: any) => next()) },
27 +
return {
28 +
getQueue: (name: string) => getQueueMock(name),
29 +
closeAllQueues: vi.fn(),
30 +
}
31 +
})
32 +
33 +
// avatar.ts constructs multer-s3 at import time and requires S3_BUCKET env var.
34 +
// The replacement is a no-op middleware unless a spec stages a file through
35 +
// setUploadedFile (see ./mocks/avatar)
36 +
vi.mock('@app/services/avatar', async () => {
37 +
const { avatarMulterMock } = await import('./mocks/avatar')
38 +
39 +
return { default: avatarMulterMock }
40 +
})
41 +
42 +
// The avatar controller deletes the previous object through the S3 client
43 +
vi.mock('@app/lib/s3', () => ({
44 +
default: { send: vi.fn().mockResolvedValue({}) },
33 45
}))