eletrotupi / tcc/ commit / 8194319

api/tests/users: cover profile update, preferences and avatar

Only create and me were exercised, so profile edits, notification
preferences and the avatar upload and removal path had no regression net
Pedro Lucas Porcellis porcellis@eletrotupi.com · 29 days ago 8194319ceb81e1e0f245796b7019fa74c1babd4e Browse files
Parents 4600798
Changed files
3
Additions
+356
Deletions
0
/dev/null → api/tests/users/avatar.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
2 +
import request from 'supertest'
3 +
import { DeleteObjectCommand } from '@aws-sdk/client-s3'
4 +
import { createApp } from '@app/createApp'
5 +
import s3 from '@app/lib/s3'
6 +
import {
7 +
  buildPrisma,
8 +
  cleanupTestDb,
9 +
  createTestUser,
10 +
  generateTestToken,
11 +
} from '../test-helper'
12 +

13 +
import { resetAvatarMock, setUploadedFile } from '../mocks/avatar'
14 +

15 +
const app = createApp()
16 +
const db = buildPrisma()
17 +
const mockedSend = vi.mocked(s3.send)
18 +

19 +
afterAll(() => db.$disconnect())
20 +

21 +
beforeEach(async () => {
22 +
  await cleanupTestDb(db)
23 +

24 +
  resetAvatarMock()
25 +
  mockedSend.mockClear()
26 +
})
27 +

28 +
const uploadedFile = {
29 +
  location: 'https://cdn.example.com/avatars/new.png',
30 +
  key: 'avatars/new.png',
31 +
  originalname: 'new.png',
32 +
  size: 1234,
33 +
  mimetype: 'image/png',
34 +
}
35 +

36 +
describe('POST /users/me/avatar', () => {
37 +
  it('stores the uploaded file on the user', async () => {
38 +
    const user = await createTestUser(db)
39 +
    const token = generateTestToken(user.id, user.email)
40 +

41 +
    setUploadedFile(uploadedFile)
42 +

43 +
    const res = await request(app)
44 +
      .post('/users/me/avatar')
45 +
      .set('Authorization', `Bearer ${token}`)
46 +

47 +
    expect(res.status).toBe(200)
48 +
    expect(res.body.key).toBe('avatars/new.png')
49 +
    expect(res.body.user.avatarURL).toBe(uploadedFile.location)
50 +
    expect(res.body.user.avatarKey).toBe('avatars/new.png')
51 +

52 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
53 +

54 +
    expect(dbUser!.avatarURL).toBe(uploadedFile.location)
55 +
  })
56 +

57 +
  it('deletes the previous object when replacing an avatar', async () => {
58 +
    const user = await createTestUser(db)
59 +
    await db.user.update({
60 +
      where: { id: user.id },
61 +
      data: {
62 +
        avatarKey: 'avatars/old.png',
63 +
        avatarURL: 'https://cdn.example.com/avatars/old.png'
64 +
      },
65 +
    })
66 +

67 +
    const token = generateTestToken(user.id, user.email)
68 +
    setUploadedFile(uploadedFile)
69 +

70 +
    const res = await request(app)
71 +
      .post('/users/me/avatar')
72 +
      .set('Authorization', `Bearer ${token}`)
73 +

74 +
    expect(res.status).toBe(200)
75 +
    expect(mockedSend).toHaveBeenCalledOnce()
76 +
    expect(mockedSend).toHaveBeenCalledWith(expect.any(DeleteObjectCommand))
77 +
  })
78 +

79 +
  it('returns 400 when no file is present', async () => {
80 +
    const user = await createTestUser(db)
81 +
    const token = generateTestToken(user.id, user.email)
82 +

83 +
    const res = await request(app)
84 +
      .post('/users/me/avatar')
85 +
      .set('Authorization', `Bearer ${token}`)
86 +

87 +
    expect(res.status).toBe(400)
88 +
    expect(mockedSend).not.toHaveBeenCalled()
89 +
  })
90 +

91 +
  it('returns 401 without a token', async () => {
92 +
    setUploadedFile(uploadedFile)
93 +

94 +
    const res = await request(app).post('/users/me/avatar')
95 +
    expect(res.status).toBe(401)
96 +
  })
97 +
})
98 +

99 +
describe('DELETE /users/me/avatar', () => {
100 +
  it('clears the stored avatar and deletes the object', async () => {
101 +
    const user = await createTestUser(db)
102 +

103 +
    await db.user.update({
104 +
      where: { id: user.id },
105 +
      data: {
106 +
        avatarKey: 'avatars/old.png',
107 +
        avatarURL: 'https://cdn.example.com/avatars/old.png'
108 +
      },
109 +
    })
110 +
    const token = generateTestToken(user.id, user.email)
111 +

112 +
    const res = await request(app)
113 +
      .delete('/users/me/avatar')
114 +
      .set('Authorization', `Bearer ${token}`)
115 +

116 +
    expect(res.status).toBe(200)
117 +
    expect(res.body.user.avatarKey).toBeNull()
118 +
    expect(res.body.user.avatarURL).toBeNull()
119 +
    expect(mockedSend).toHaveBeenCalledOnce()
120 +

121 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
122 +

123 +
    expect(dbUser!.avatarKey).toBeNull()
124 +
  })
125 +

126 +
  it('is a no-op when there is no avatar to remove', async () => {
127 +
    const user = await createTestUser(db)
128 +
    const token = generateTestToken(user.id, user.email)
129 +

130 +
    const res = await request(app)
131 +
      .delete('/users/me/avatar')
132 +
      .set('Authorization', `Bearer ${token}`)
133 +

134 +
    expect(res.status).toBe(200)
135 +
    expect(res.body.user.id).toBe(user.id)
136 +
    expect(mockedSend).not.toHaveBeenCalled()
137 +
  })
138 +

139 +
  it('returns 401 without a token', async () => {
140 +
    const res = await request(app).delete('/users/me/avatar')
141 +
    expect(res.status).toBe(401)
142 +
  })
143 +
})
/dev/null → api/tests/users/preferences.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
import { DailyReminderJobName } from '@app/lib/queue/types'
5 +
import {
6 +
  buildPrisma,
7 +
  cleanupTestDb,
8 +
  createTestUser,
9 +
  generateTestToken,
10 +
} from '../test-helper'
11 +

12 +
import { recordedJobs, resetQueueMocks } from '../mocks/queue'
13 +

14 +
const app = createApp()
15 +
const db = buildPrisma()
16 +

17 +
afterAll(() => db.$disconnect())
18 +
beforeEach(async () => {
19 +
  await cleanupTestDb(db)
20 +

21 +
  resetQueueMocks()
22 +
})
23 +

24 +
describe('PATCH /users/me', () => {
25 +
  it('updates notification preferences and schedules a reminder', async () => {
26 +
    const user = await createTestUser(db, { email: 'prefs@example.com' })
27 +
    const token = generateTestToken(user.id, user.email)
28 +

29 +
    const res = await request(app)
30 +
      .patch('/users/me')
31 +
      .set('Authorization', `Bearer ${token}`)
32 +
      .send({ user: { notificationsEnabled: true, dailyReminderTime: '08:30' } })
33 +

34 +
    expect(res.status).toBe(200)
35 +
    expect(res.body.user.notificationsEnabled).toBe(true)
36 +
    expect(res.body.user.dailyReminderTime).toBe('08:30')
37 +

38 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
39 +
    expect(dbUser!.dailyReminderTime).toBe('08:30')
40 +

41 +
    expect(recordedJobs).toContainEqual(
42 +
      expect.objectContaining({
43 +
        queue: 'daily-reminders',
44 +
        name: DailyReminderJobName.Send,
45 +
        data: { userId: user.id },
46 +
      })
47 +
    )
48 +
  })
49 +

50 +
  it('accepts a null reminder time and schedules nothing', async () => {
51 +
    const user = await createTestUser(db, { email: 'prefs@example.com' })
52 +
    const token = generateTestToken(user.id, user.email)
53 +

54 +
    const res = await request(app)
55 +
      .patch('/users/me')
56 +
      .set('Authorization', `Bearer ${token}`)
57 +
      .send({ user: { dailyReminderTime: null } })
58 +

59 +
    expect(res.status).toBe(200)
60 +
    expect(res.body.user.dailyReminderTime).toBeNull()
61 +
    expect(recordedJobs).toHaveLength(0)
62 +
  })
63 +

64 +
  it('returns 400 for a malformed time', async () => {
65 +
    const user = await createTestUser(db, { email: 'prefs@example.com' })
66 +
    const token = generateTestToken(user.id, user.email)
67 +

68 +
    const res = await request(app)
69 +
      .patch('/users/me')
70 +
      .set('Authorization', `Bearer ${token}`)
71 +
      .send({ user: { dailyReminderTime: '9:5' } })
72 +

73 +
    expect(res.status).toBe(400)
74 +
    expect(res.body.fields).toBeTruthy()
75 +
  })
76 +

77 +
  it('returns 400 for a non-boolean notificationsEnabled', async () => {
78 +
    const user = await createTestUser(db, { email: 'prefs@example.com' })
79 +
    const token = generateTestToken(user.id, user.email)
80 +

81 +
    const res = await request(app)
82 +
      .patch('/users/me')
83 +
      .set('Authorization', `Bearer ${token}`)
84 +
      .send({ user: { notificationsEnabled: 'yes' } })
85 +

86 +
    expect(res.status).toBe(400)
87 +
  })
88 +

89 +
  it('returns 401 without a token', async () => {
90 +
    const res = await request(app)
91 +
      .patch('/users/me')
92 +
      .send({ user: { notificationsEnabled: false } })
93 +

94 +
    expect(res.status).toBe(401)
95 +
  })
96 +
})
/dev/null → api/tests/users/update.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
import {
5 +
  buildPrisma,
6 +
  cleanupTestDb,
7 +
  createTestUser,
8 +
  generateTestToken,
9 +
} from '../test-helper'
10 +

11 +
const app = createApp()
12 +
const db = buildPrisma()
13 +

14 +
afterAll(() => db.$disconnect())
15 +
beforeEach(() => cleanupTestDb(db))
16 +

17 +
describe('PUT /users/:id', () => {
18 +
  it('updates the authenticated user without leaking secrets', async () => {
19 +
    const user = await createTestUser(db, {
20 +
      email: 'profile@example.com',
21 +
      firstName: 'Old',
22 +
    })
23 +

24 +
    const token = generateTestToken(user.id, user.email)
25 +

26 +
    const res = await request(app)
27 +
      .put(`/users/${user.id}`)
28 +
      .set('Authorization', `Bearer ${token}`)
29 +
      .send({
30 +
        user: {
31 +
          id: user.id,
32 +
          firstName: 'New',
33 +
          lastName: 'Name',
34 +
        },
35 +
      })
36 +

37 +
    expect(res.status).toBe(200)
38 +
    expect(res.body.user.firstName).toBe('New')
39 +
    expect(res.body.user.lastName).toBe('Name')
40 +
    expect(res.body.user.encryptedPassword).toBeUndefined()
41 +
    expect(res.body.user.passwordResetToken).toBeUndefined()
42 +

43 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
44 +
    expect(dbUser!.firstName).toBe('New')
45 +
  })
46 +

47 +
  it('returns 401 without a token', async () => {
48 +
    const user = await createTestUser(db)
49 +

50 +
    const res = await request(app)
51 +
      .put(`/users/${user.id}`)
52 +
      .send({ user: { id: user.id, firstName: 'Nope' } })
53 +

54 +
    expect(res.status).toBe(401)
55 +

56 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
57 +
    expect(dbUser!.firstName).not.toBe('Nope')
58 +
  })
59 +

60 +
  it('forbids updating another user', async () => {
61 +
    const user = await createTestUser(db)
62 +
    const victim = await createTestUser(db, {
63 +
      email: 'victim@example.com',
64 +
      firstName: 'Victim',
65 +
    })
66 +

67 +
    const token = generateTestToken(user.id, user.email)
68 +

69 +
    const res = await request(app)
70 +
      .put(`/users/${victim.id}`)
71 +
      .set('Authorization', `Bearer ${token}`)
72 +
      .send({ user: { id: victim.id, firstName: 'Hacked' } })
73 +

74 +
    expect(res.status).toBe(403)
75 +

76 +
    const dbVictim = await db.user.findUnique({ where: { id: victim.id } })
77 +

78 +
    expect(dbVictim!.firstName).toBe('Victim')
79 +
  })
80 +

81 +
  it('returns 404 when the user no longer exists', async () => {
82 +
    const token = generateTestToken(999999, 'ghost@example.com')
83 +

84 +
    const res = await request(app)
85 +
      .put('/users/999999')
86 +
      .set('Authorization', `Bearer ${token}`)
87 +
      .send({ user: { id: 999999, firstName: 'Ghost' } })
88 +

89 +
    expect(res.status).toBe(404)
90 +
  })
91 +

92 +
  it('returns 400 for an invalid email', async () => {
93 +
    const user = await createTestUser(db)
94 +
    const token = generateTestToken(user.id, user.email)
95 +

96 +
    const res = await request(app)
97 +
      .put(`/users/${user.id}`)
98 +
      .set('Authorization', `Bearer ${token}`)
99 +
      .send({ user: { id: user.id, email: 'not-an-email' } })
100 +

101 +
    expect(res.status).toBe(400)
102 +
    expect(res.body.fields).toBeTruthy()
103 +
  })
104 +

105 +
  it('returns 409 when the email is already taken', async () => {
106 +
    const user = await createTestUser(db, { email: 'mine@example.com' })
107 +
    await createTestUser(db, { email: 'taken@example.com' })
108 +
    const token = generateTestToken(user.id, user.email)
109 +

110 +
    const res = await request(app)
111 +
      .put(`/users/${user.id}`)
112 +
      .set('Authorization', `Bearer ${token}`)
113 +
      .send({ user: { id: user.id, email: 'taken@example.com' } })
114 +

115 +
    expect(res.status).toBe(409)
116 +
  })
117 +
})