api/tests/insights: add coverage for the insights route
It was the only route module without a spec, leaving filtering, ordering, ownership and query validation unguarded Still need to patch the actual processors tho
Pedro Lucas Porcellis porcellis@eletrotupi.com · 29 days ago
46007980e4cde4c1bbec8742cbce524254e85542 Browse filesParents
4456661Changed files
1
Additions
+166
Deletions
0
/dev/null → api/tests/insights/index.test.ts
1 +
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
import { InsightPeriod, InsightType } from '@prisma/client'
5 +
import {
6 +
buildPrisma,
7 +
cleanupTestDb,
8 +
createTestUser,
9 +
generateTestToken,
10 +
} from '../test-helper'
11 +
12 +
const app = createApp()
13 +
const db = buildPrisma()
14 +
15 +
afterAll(() => db.$disconnect())
16 +
beforeEach(() => cleanupTestDb(db))
17 +
18 +
let seedCounter = 0
19 +
// The schema is unique on (userId, type, periodStart), so give every seeded
20 +
// row a distinct periodStart unless the test asks for a specific one
21 +
function uniquePeriodStart() {
22 +
seedCounter += 1
23 +
return new Date(Date.UTC(2026, 0, 1) + seedCounter * 60 * 1000)
24 +
}
25 +
26 +
async function seedInsight(
27 +
userId: number,
28 +
overrides: {
29 +
type?: InsightType
30 +
period?: InsightPeriod
31 +
periodStart?: Date
32 +
generatedAt?: Date
33 +
title?: string
34 +
} = {}
35 +
) {
36 +
seedCounter += 1
37 +
38 +
return db.insight.create({
39 +
data: {
40 +
userId,
41 +
type: overrides.type ?? InsightType.MOOD_TREND,
42 +
period: overrides.period ?? InsightPeriod.WEEKLY,
43 +
title: overrides.title ?? `Insight ${seedCounter}`,
44 +
body: 'Generated by the insight worker',
45 +
periodStart: overrides.periodStart ?? uniquePeriodStart(),
46 +
periodEnd: new Date(),
47 +
generatedAt: overrides.generatedAt,
48 +
},
49 +
})
50 +
}
51 +
52 +
describe('GET /insights', () => {
53 +
it('returns only the authenticated user\'s insights, newest first', async () => {
54 +
const user = await createTestUser(db)
55 +
const other = await createTestUser(db, { email: 'other@example.com' })
56 +
const token = generateTestToken(user.id, user.email)
57 +
58 +
await seedInsight(user.id, { title: 'oldest', generatedAt: new Date('2026-01-01') })
59 +
await seedInsight(user.id, { title: 'newest', generatedAt: new Date('2026-03-01') })
60 +
await seedInsight(user.id, { title: 'middle', generatedAt: new Date('2026-02-01') })
61 +
await seedInsight(other.id, { title: 'not-mine' })
62 +
63 +
const res = await request(app)
64 +
.get('/insights')
65 +
.set('Authorization', `Bearer ${token}`)
66 +
67 +
expect(res.status).toBe(200)
68 +
expect(res.body.insights).toHaveLength(3)
69 +
expect(res.body.insights.every((i: any) => i.userId === user.id)).toBe(true)
70 +
expect(res.body.insights.map((i: any) => i.title)).toEqual([
71 +
'newest',
72 +
'middle',
73 +
'oldest',
74 +
])
75 +
})
76 +
77 +
it('returns an empty list when the user has no insights', async () => {
78 +
const user = await createTestUser(db)
79 +
const token = generateTestToken(user.id, user.email)
80 +
81 +
const res = await request(app)
82 +
.get('/insights')
83 +
.set('Authorization', `Bearer ${token}`)
84 +
85 +
expect(res.status).toBe(200)
86 +
expect(res.body.insights).toEqual([])
87 +
})
88 +
89 +
it('filters by type', async () => {
90 +
const user = await createTestUser(db)
91 +
const token = generateTestToken(user.id, user.email)
92 +
93 +
await seedInsight(user.id, { type: InsightType.MOOD_TREND })
94 +
await seedInsight(user.id, { type: InsightType.STREAK })
95 +
await seedInsight(user.id, { type: InsightType.STREAK })
96 +
97 +
const res = await request(app)
98 +
.get('/insights?type=STREAK')
99 +
.set('Authorization', `Bearer ${token}`)
100 +
101 +
expect(res.status).toBe(200)
102 +
expect(res.body.insights).toHaveLength(2)
103 +
expect(res.body.insights.every((i: any) => i.type === 'STREAK')).toBe(true)
104 +
})
105 +
106 +
it('filters by period', async () => {
107 +
const user = await createTestUser(db)
108 +
const token = generateTestToken(user.id, user.email)
109 +
110 +
await seedInsight(user.id, { period: InsightPeriod.DAILY })
111 +
await seedInsight(user.id, { period: InsightPeriod.MONTHLY })
112 +
await seedInsight(user.id, { period: InsightPeriod.MONTHLY })
113 +
114 +
const res = await request(app)
115 +
.get('/insights?period=MONTHLY')
116 +
.set('Authorization', `Bearer ${token}`)
117 +
118 +
expect(res.status).toBe(200)
119 +
expect(res.body.insights).toHaveLength(2)
120 +
expect(res.body.insights.every((i: any) => i.period === 'MONTHLY')).toBe(true)
121 +
})
122 +
123 +
it('caps the result set at the requested limit', async () => {
124 +
const user = await createTestUser(db)
125 +
const token = generateTestToken(user.id, user.email)
126 +
127 +
await seedInsight(user.id)
128 +
await seedInsight(user.id)
129 +
await seedInsight(user.id)
130 +
131 +
const res = await request(app)
132 +
.get('/insights?limit=2')
133 +
.set('Authorization', `Bearer ${token}`)
134 +
135 +
expect(res.status).toBe(200)
136 +
expect(res.body.insights).toHaveLength(2)
137 +
})
138 +
139 +
it('returns 400 for an unknown type', async () => {
140 +
const user = await createTestUser(db)
141 +
const token = generateTestToken(user.id, user.email)
142 +
143 +
const res = await request(app)
144 +
.get('/insights?type=NOPE')
145 +
.set('Authorization', `Bearer ${token}`)
146 +
147 +
expect(res.status).toBe(400)
148 +
expect(res.body.fields).toBeTruthy()
149 +
})
150 +
151 +
it('returns 400 for a non-positive limit', async () => {
152 +
const user = await createTestUser(db)
153 +
const token = generateTestToken(user.id, user.email)
154 +
155 +
const res = await request(app)
156 +
.get('/insights?limit=0')
157 +
.set('Authorization', `Bearer ${token}`)
158 +
159 +
expect(res.status).toBe(400)
160 +
})
161 +
162 +
it('returns 401 without a token', async () => {
163 +
const res = await request(app).get('/insights')
164 +
expect(res.status).toBe(401)
165 +
})
166 +
})