| 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 | })
|