eletrotupi / tcc/ commit / bfd15c2

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 files
Parents 7c99110
Changed 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 modified+38 -26
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
}))