eletrotupi / tcc/ api/tests/jobs/export-data.test.ts19821da369359c6ba1cc9b4afa45b47d0e72f116
4.6 KB | 139 lines Raw
1
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
2
import JSZip from 'jszip'
3
import { exportProcessor } from '@app/lib/queue/processors/exports'
4
import { ExportJobName } from '@app/lib/queue/types'
5
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
6

7
// Intercept all Resend calls
8
vi.mock('@app/lib/mail', () => ({
9
  sendEmail: vi.fn().mockResolvedValue({
10
    data: { id: 'fake-email-id' },
11
    error: null,
12
  }),
13
}))
14

15
import { sendEmail } from '@app/lib/mail'
16
const mockedSendEmail = vi.mocked(sendEmail)
17

18
const db = buildPrisma()
19

20
const buildJob = (data: unknown, overrides: Record<string, unknown> = {}) =>
21
  ({
22
    name: ExportJobName.Data,
23
    data,
24
    attemptsMade: 0,
25
    opts: { attempts: 3 },
26
    ...overrides,
27
  }) as any
28

29
async function attachmentFromLastEmail() {
30
  const calls = mockedSendEmail.mock.calls
31
  const [payload] = calls[calls.length - 1]
32
  const [attachment] = payload.attachments!
33

34
  return { payload, attachment }
35
}
36

37
afterAll(() => db.$disconnect())
38
beforeEach(async () => {
39
  await cleanupTestDb(db)
40
  mockedSendEmail.mockClear()
41
  mockedSendEmail.mockResolvedValue({ data: { id: 'fake-email-id' }, error: null } as any)
42
})
43

44
describe('exportProcessor', () => {
45
  it('emails a zip with the profile CSV and a manifest', async () => {
46
    const user = await createTestUser(db, {
47
      email: 'export@example.com',
48
      firstName: 'Ana',
49
      lastName: 'Silva',
50
    })
51

52
    await exportProcessor(buildJob({ userId: user.id }))
53

54
    expect(mockedSendEmail).toHaveBeenCalledOnce()
55

56
    const { payload, attachment } = await attachmentFromLastEmail()
57
    expect(payload.to).toBe('export@example.com')
58
    expect(payload.subject).toBe('Seus dados do nexo')
59
    expect(attachment.filename).toMatch(/^nexo-dados-\d{4}-\d{2}-\d{2}\.zip$/)
60

61
    const zip = await JSZip.loadAsync(attachment.content)
62
    expect(Object.keys(zip.files).sort()).toEqual(['leia-me.md', 'profile.csv'])
63

64
    const csv = await zip.file('profile.csv')!.async('string')
65
    expect(csv.startsWith('\ufeff')).toBe(true)
66

67
    const lines = csv.slice(1).split('\r\n').filter(Boolean)
68
    expect(lines).toHaveLength(2)
69
    expect(lines[0]).toContain('Nome')
70
    expect(lines[0]).toContain('Email')
71

72
    const [row] = lines.slice(1)
73
    expect(row).toContain('"Ana"')
74
    expect(row).toContain('"Silva"')
75
    expect(row).toContain('"export@example.com"')
76
    expect(row).toContain('"Sim"') // notifications default to enabled
77
    expect(row).toContain('"Não"') // freshly created users are inactive
78

79
    // Secrets must never travel in the export
80
    const lower = csv.toLowerCase()
81
    expect(lower).not.toContain('senha')
82
    expect(lower).not.toContain('token')
83
    expect(lower).not.toContain('push')
84
    expect(lower).not.toContain('avatar')
85

86
    const manifest = await zip.file('leia-me.md')!.async('string')
87
    expect(manifest).toContain('`profile.csv`')
88
    expect(manifest).toContain('1 registro')
89
    expect(manifest).toContain('_id')
90
  })
91

92
  it('preserves commas, quotes and newlines inside a field', async () => {
93
    const user = await createTestUser(db, {
94
      email: 'quoted@example.com',
95
      lastName: 'Silva, "S"\nJunior',
96
    })
97

98
    await exportProcessor(buildJob({ userId: user.id }))
99

100
    const { attachment } = await attachmentFromLastEmail()
101
    const zip = await JSZip.loadAsync(attachment.content)
102
    const csv = await zip.file('profile.csv')!.async('string')
103

104
    expect(csv).toContain('"Silva, ""S""\nJunior"')
105
  })
106

107
  it('sends the failure email only on the final attempt', async () => {
108
    const user = await createTestUser(db, { email: 'fail@example.com' })
109
    mockedSendEmail
110
      .mockRejectedValueOnce(new Error('boom'))
111
      .mockResolvedValueOnce({ data: { id: 'fake-email-id' }, error: null })
112

113
    await expect(
114
      exportProcessor(buildJob({ userId: user.id }, { attemptsMade: 2, opts: { attempts: 3 } }))
115
    ).rejects.toThrow('boom')
116

117
    expect(mockedSendEmail).toHaveBeenCalledTimes(2)
118
    expect(mockedSendEmail.mock.calls[1][0].subject).toBe(
119
      'Não conseguimos exportar seus dados'
120
    )
121
  })
122

123
  it('does not send the failure email on a non-final attempt', async () => {
124
    const user = await createTestUser(db, { email: 'retry@example.com' })
125
    mockedSendEmail.mockRejectedValueOnce(new Error('boom'))
126

127
    await expect(
128
      exportProcessor(buildJob({ userId: user.id }, { attemptsMade: 0, opts: { attempts: 3 } }))
129
    ).rejects.toThrow('boom')
130

131
    expect(mockedSendEmail).toHaveBeenCalledOnce()
132
  })
133

134
  it('throws for unknown job names', async () => {
135
    await expect(
136
      exportProcessor({ name: 'export:unknown', data: {} } as any)
137
    ).rejects.toThrow('Unknown export job')
138
  })
139
})