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 filesParents
bfd15c2Changed 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 +
})