eletrotupi / tcc/ commit / 4456661

api/tests/auth: cover reset, activation and verification

Login was the only auth flow exercised, so password reset, account
activation, code resend and token verification could regress unnoticed
Pedro Lucas Porcellis porcellis@eletrotupi.com · 29 days ago 44566618da638909dc860baf5a02025c61568c46 Browse files
Parents bfd15c2
Changed files
4
Additions
+302
Deletions
0
/dev/null → api/tests/auth/activate.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 +
async function seedActivationCode(
18 +
  userId: number,
19 +
  code = '123456',
20 +
  expiresInMs = 5 * 60 * 1000
21 +
) {
22 +
  return db.user.update({
23 +
    where: { id: userId },
24 +
    data: {
25 +
      activationCode: code,
26 +
      activationCodeExpiresAt: new Date(Date.now() + expiresInMs),
27 +
    },
28 +
  })
29 +
}
30 +

31 +
describe('POST /auth/activate', () => {
32 +
  it('activates the user and clears the code', async () => {
33 +
    const user = await createTestUser(db, { email: 'activate@example.com' })
34 +
    await seedActivationCode(user.id)
35 +

36 +
    const res = await request(app)
37 +
      .post('/auth/activate')
38 +
      .send({ code: 123456 })
39 +

40 +
    expect(res.status).toBe(200)
41 +
    expect(res.body.user.active).toBe(true)
42 +
    expect(res.body.user.activationCode).toBeNull()
43 +

44 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
45 +
    expect(dbUser!.active).toBe(true)
46 +
    expect(dbUser!.activationCode).toBeNull()
47 +
  })
48 +

49 +
  it('returns 422 for an unknown code', async () => {
50 +
    await createTestUser(db, { email: 'activate@example.com' })
51 +

52 +
    const res = await request(app)
53 +
      .post('/auth/activate')
54 +
      .send({ code: 999999 })
55 +

56 +
    expect(res.status).toBe(422)
57 +
    expect(res.body.error).toBeTruthy()
58 +
  })
59 +

60 +
  it('returns 400 when the code is not a number', async () => {
61 +
    const res = await request(app)
62 +
      .post('/auth/activate')
63 +
      .send({ code: '123456' })
64 +

65 +
    expect(res.status).toBe(400)
66 +
    expect(res.body.fields).toBeTruthy()
67 +
  })
68 +
})
69 +

70 +
describe('POST /auth/resend_code', () => {
71 +
  it('issues a fresh activation code for the authenticated user', async () => {
72 +
    const user = await createTestUser(db, { email: 'resend@example.com' })
73 +
    const token = generateTestToken(user.id, user.email)
74 +

75 +
    const res = await request(app)
76 +
      .post('/auth/resend_code')
77 +
      .set('Authorization', `Bearer ${token}`)
78 +

79 +
    expect(res.status).toBe(200)
80 +

81 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
82 +
    expect(dbUser!.activationCode).toMatch(/^\d{6}$/)
83 +
    expect(dbUser!.activationCodeExpiresAt).not.toBeNull()
84 +
  })
85 +

86 +
  it('returns 401 without a token', async () => {
87 +
    const res = await request(app).post('/auth/resend_code')
88 +
    expect(res.status).toBe(401)
89 +
  })
90 +
})
/dev/null → api/tests/auth/forgot-password.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
5 +
import { recordedJobs, resetQueueMocks } from '../mocks/queue'
6 +

7 +
const app = createApp()
8 +
const db = buildPrisma()
9 +

10 +
afterAll(() => db.$disconnect())
11 +
beforeEach(async () => {
12 +
  await cleanupTestDb(db)
13 +

14 +
  resetQueueMocks()
15 +
})
16 +

17 +
describe('POST /auth/forgot-password', () => {
18 +
  it('stores a reset token and enqueues the email for a known user', async () => {
19 +
    const user = await createTestUser(db, { email: 'forgot@example.com' })
20 +

21 +
    const res = await request(app)
22 +
      .post('/auth/forgot-password')
23 +
      .send({ email: 'forgot@example.com' })
24 +

25 +
    expect(res.status).toBe(200)
26 +
    expect(res.body.success).toBe(true)
27 +
    expect(res.body.token).toBeTruthy()
28 +

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

31 +
    expect(dbUser!.passwordResetToken).toBe(res.body.token)
32 +
    expect(dbUser!.passwordResetExpires).not.toBeNull()
33 +

34 +
    expect(recordedJobs).toContainEqual(
35 +
      expect.objectContaining({
36 +
        queue: 'mail',
37 +
        name: 'mail:password-reset',
38 +
        data: { userId: user.id, token: res.body.token },
39 +
      })
40 +
    )
41 +
  })
42 +

43 +
  it('answers 200 without a token for an unknown email (no enumeration)', async () => {
44 +
    const res = await request(app)
45 +
      .post('/auth/forgot-password')
46 +
      .send({ email: 'ghost@example.com' })
47 +

48 +
    expect(res.status).toBe(200)
49 +
    expect(res.body.success).toBe(true)
50 +
    expect(res.body.token).toBeUndefined()
51 +
    expect(recordedJobs).toHaveLength(0)
52 +
  })
53 +

54 +
  it('returns 400 for an invalid email', async () => {
55 +
    const res = await request(app)
56 +
      .post('/auth/forgot-password')
57 +
      .send({ email: 'not-an-email' })
58 +

59 +
    expect(res.status).toBe(400)
60 +
    expect(res.body.error).toBeTruthy()
61 +
    expect(res.body.fields).toBeTruthy()
62 +
  })
63 +
})
/dev/null → api/tests/auth/reset-password.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
5 +

6 +
const app = createApp()
7 +
const db = buildPrisma()
8 +

9 +
afterAll(() => db.$disconnect())
10 +
beforeEach(() => cleanupTestDb(db))
11 +

12 +
async function seedResetToken(
13 +
  userId: number,
14 +
  overrides: { token?: string; expiresAt?: Date } = {}
15 +
) {
16 +
  return db.user.update({
17 +
    where: { id: userId },
18 +
    data: {
19 +
      passwordResetToken: overrides.token ?? 'reset-token-123',
20 +
      passwordResetExpires: overrides.expiresAt ?? new Date(Date.now() + 60 * 60 * 1000),
21 +
    },
22 +
  })
23 +
}
24 +

25 +
describe('POST /auth/reset-password', () => {
26 +
  it('replaces the password and clears the reset token', async () => {
27 +
    const user = await createTestUser(db, {
28 +
      email: 'reset@example.com',
29 +
      password: 'old-password',
30 +
    })
31 +

32 +
    await seedResetToken(user.id)
33 +

34 +
    const res = await request(app)
35 +
      .post('/auth/reset-password')
36 +
      .send({ token: 'reset-token-123', newPassword: 'brand-new-password' })
37 +

38 +
    expect(res.status).toBe(200)
39 +
    expect(res.body.success).toBe(true)
40 +

41 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
42 +
    expect(dbUser!.passwordResetToken).toBeNull()
43 +
    expect(dbUser!.passwordResetExpires).toBeNull()
44 +

45 +
    const login = await request(app)
46 +
      .post('/auth/login')
47 +
      .send({ email: 'reset@example.com', password: 'brand-new-password' })
48 +

49 +
    expect(login.status).toBe(200)
50 +

51 +
    const oldLogin = await request(app)
52 +
      .post('/auth/login')
53 +
      .send({ email: 'reset@example.com', password: 'old-password' })
54 +

55 +
    expect(oldLogin.status).toBe(401)
56 +
  })
57 +

58 +
  it('returns 400 for an expired token', async () => {
59 +
    const user = await createTestUser(db, { email: 'expired@example.com' })
60 +
    await seedResetToken(user.id, {
61 +
      token: 'stale-token',
62 +
      expiresAt: new Date(Date.now() - 60 * 1000),
63 +
    })
64 +

65 +
    const res = await request(app)
66 +
      .post('/auth/reset-password')
67 +
      .send({ token: 'stale-token', newPassword: 'whatever-123' })
68 +

69 +
    expect(res.status).toBe(400)
70 +
    expect(res.body.error).toMatch(/token/i)
71 +
  })
72 +

73 +
  it('returns 400 for an unknown token', async () => {
74 +
    const res = await request(app)
75 +
      .post('/auth/reset-password')
76 +
      .send({ token: 'does-not-exist', newPassword: 'whatever-123' })
77 +

78 +
    expect(res.status).toBe(400)
79 +
  })
80 +

81 +
  it('returns 400 for a too-short password', async () => {
82 +
    const res = await request(app)
83 +
      .post('/auth/reset-password')
84 +
      .send({ token: 'reset-token-123', newPassword: '123' })
85 +

86 +
    expect(res.status).toBe(400)
87 +
    expect(res.body.fields).toBeTruthy()
88 +
  })
89 +
})
/dev/null → api/tests/auth/verify.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('GET /auth/verify', () => {
18 +
  it('returns the token payload and a safe slice of the user', async () => {
19 +
    const user = await createTestUser(db, {
20 +
      email: 'verify@example.com',
21 +
      firstName: 'Pedro',
22 +
    })
23 +

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

26 +
    const res = await request(app)
27 +
      .get('/auth/verify')
28 +
      .set('Authorization', `Bearer ${token}`)
29 +

30 +
    expect(res.status).toBe(200)
31 +
    expect(res.body.valid).toBe(true)
32 +
    expect(res.body.userId).toBe(user.id)
33 +
    expect(res.body.email).toBe('verify@example.com')
34 +
    expect(res.body.user.firstName).toBe('Pedro')
35 +
    expect(res.body.user.encryptedPassword).toBeUndefined()
36 +
  })
37 +

38 +
  it('returns 401 without an authorization header', async () => {
39 +
    const res = await request(app).get('/auth/verify')
40 +
    expect(res.status).toBe(401)
41 +
  })
42 +

43 +
  it('returns 401 for a malformed token', async () => {
44 +
    const res = await request(app)
45 +
      .get('/auth/verify')
46 +
      .set('Authorization', 'Bearer not.a.jwt')
47 +

48 +
    expect(res.status).toBe(401)
49 +
  })
50 +

51 +
  it('returns 404 when the token is valid but the user is gone', async () => {
52 +
    const token = generateTestToken(999999, 'ghost@example.com')
53 +

54 +
    const res = await request(app)
55 +
      .get('/auth/verify')
56 +
      .set('Authorization', `Bearer ${token}`)
57 +

58 +
    expect(res.status).toBe(404)
59 +
  })
60 +
})