eletrotupi / tcc/ commit / 4600798

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 files
Parents 4456661
Changed 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 +
})