1+ import { Prisma , type PrismaClient } from '@prisma/client' ;
12import Fastify , { type FastifyInstance } from 'fastify' ;
23import { describe , it , expect , beforeEach , vi } from 'vitest' ;
34
45import { profileRoutes } from '../routes/profiles.js' ;
56
6- import type { PrismaClient } from '@prisma/client' ;
7-
87const mockUser = {
98 id : 'user-123' ,
109 email : 'test@example.com' ,
@@ -22,11 +21,27 @@ const mockUser = {
2221 providerId : 'gh-123' ,
2322} ;
2423
24+ const mockUserFindUnique = vi . fn ( ) ;
25+ const mockUserFindFirst = vi . fn ( ) ;
26+ const mockUserUpdate = vi . fn ( ) ;
27+ const mockPlatformLinkAggregate = vi . fn ( ) ;
28+ const mockPlatformLinkCreate = vi . fn ( ) ;
29+ const mockPlatformLinkFindFirst = vi . fn ( ) ;
30+ const mockPlatformLinkUpdate = vi . fn ( ) ;
31+ const mockPlatformLinkDelete = vi . fn ( ) ;
32+
2533const mockPrisma = {
2634 user : {
27- findUnique : vi . fn ( ) ,
28- findFirst : vi . fn ( ) ,
29- update : vi . fn ( ) ,
35+ findUnique : mockUserFindUnique ,
36+ findFirst : mockUserFindFirst ,
37+ update : mockUserUpdate ,
38+ } ,
39+ platformLink : {
40+ aggregate : mockPlatformLinkAggregate ,
41+ create : mockPlatformLinkCreate ,
42+ findFirst : mockPlatformLinkFindFirst ,
43+ update : mockPlatformLinkUpdate ,
44+ delete : mockPlatformLinkDelete ,
3045 } ,
3146} ;
3247
@@ -45,7 +60,7 @@ describe('GET /api/profiles/me', () => {
4560 beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
4661
4762 it ( 'should return user profile with displayName' , async ( ) => {
48- mockPrisma . user . findUnique . mockResolvedValue ( mockUser ) ;
63+ mockUserFindUnique . mockResolvedValue ( mockUser ) ;
4964 const app = await buildApp ( ) ;
5065 const res = await app . inject ( { method : 'GET' , url : '/api/profiles/me' } ) ;
5166 expect ( res . statusCode ) . toBe ( 200 ) ;
@@ -57,7 +72,7 @@ describe('GET /api/profiles/me', () => {
5772 } ) ;
5873
5974 it ( 'should return 404 if user not found' , async ( ) => {
60- mockPrisma . user . findUnique . mockResolvedValue ( null ) ;
75+ mockUserFindUnique . mockResolvedValue ( null ) ;
6176 const app = await buildApp ( ) ;
6277 const res = await app . inject ( { method : 'GET' , url : '/api/profiles/me' } ) ;
6378 expect ( res . statusCode ) . toBe ( 404 ) ;
@@ -69,8 +84,8 @@ describe('PUT /api/profiles/me', () => {
6984 beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
7085
7186 it ( 'should update profile and return updated data' , async ( ) => {
72- mockPrisma . user . findFirst . mockResolvedValue ( null ) ;
73- mockPrisma . user . update . mockResolvedValue ( { ...mockUser , displayName : 'Updated Name' } ) ;
87+ mockUserFindFirst . mockResolvedValue ( null ) ;
88+ mockUserUpdate . mockResolvedValue ( { ...mockUser , displayName : 'Updated Name' } ) ;
7489 const app = await buildApp ( ) ;
7590 const res = await app . inject ( {
7691 method : 'PUT' ,
@@ -90,10 +105,11 @@ describe('PUT /api/profiles/me', () => {
90105 } ) ;
91106 expect ( res . statusCode ) . toBe ( 400 ) ;
92107 expect ( res . json ( ) . error ) . toBe ( 'Validation failed' ) ;
108+ expect ( mockUserUpdate ) . not . toHaveBeenCalled ( ) ;
93109 } ) ;
94110
95- it ( 'should return 409 if username is already taken (pre-check) ' , async ( ) => {
96- mockPrisma . user . findFirst . mockResolvedValue ( { id : 'other-user' } ) ;
111+ it ( 'should return 409 if username is already taken' , async ( ) => {
112+ mockUserFindFirst . mockResolvedValue ( { id : 'other-user' } ) ;
97113 const app = await buildApp ( ) ;
98114 const res = await app . inject ( {
99115 method : 'PUT' ,
@@ -102,14 +118,18 @@ describe('PUT /api/profiles/me', () => {
102118 } ) ;
103119 expect ( res . statusCode ) . toBe ( 409 ) ;
104120 expect ( res . json ( ) . error ) . toBe ( 'Username already taken' ) ;
121+ expect ( mockUserUpdate ) . not . toHaveBeenCalled ( ) ;
105122 } ) ;
106123
107124 it ( 'should return 409 when a concurrent request wins the unique constraint race (P2002)' , async ( ) => {
108125 // Both requests pass the findFirst check; the DB unique constraint fires on
109126 // the losing write — Prisma raises P2002.
110- mockPrisma . user . findFirst . mockResolvedValue ( null ) ;
111- const p2002 = Object . assign ( new Error ( 'Unique constraint failed' ) , { code : 'P2002' } ) ;
112- mockPrisma . user . update . mockRejectedValue ( p2002 ) ;
127+ mockUserFindFirst . mockResolvedValue ( null ) ;
128+ const p2002 = new Prisma . PrismaClientKnownRequestError ( 'Unique constraint failed' , {
129+ code : 'P2002' ,
130+ clientVersion : 'test' ,
131+ } ) ;
132+ mockUserUpdate . mockRejectedValue ( p2002 ) ;
113133
114134 const app = await buildApp ( ) ;
115135 const res = await app . inject ( {
@@ -123,8 +143,8 @@ describe('PUT /api/profiles/me', () => {
123143 } ) ;
124144
125145 it ( 'should return 500 for unexpected database errors during update' , async ( ) => {
126- mockPrisma . user . findFirst . mockResolvedValue ( null ) ;
127- mockPrisma . user . update . mockRejectedValue ( new Error ( 'Connection refused' ) ) ;
146+ mockUserFindFirst . mockResolvedValue ( null ) ;
147+ mockUserUpdate . mockRejectedValue ( new Error ( 'Connection refused' ) ) ;
128148
129149 const app = await buildApp ( ) ;
130150 const res = await app . inject ( {
@@ -138,7 +158,7 @@ describe('PUT /api/profiles/me', () => {
138158 } ) ;
139159
140160 it ( 'should not call findFirst when no username is provided in the update' , async ( ) => {
141- mockPrisma . user . update . mockResolvedValue ( { ...mockUser , displayName : 'New Name' } ) ;
161+ mockUserUpdate . mockResolvedValue ( { ...mockUser , displayName : 'New Name' } ) ;
142162 const app = await buildApp ( ) ;
143163 const res = await app . inject ( {
144164 method : 'PUT' ,
@@ -147,6 +167,103 @@ describe('PUT /api/profiles/me', () => {
147167 } ) ;
148168
149169 expect ( res . statusCode ) . toBe ( 200 ) ;
150- expect ( mockPrisma . user . findFirst ) . not . toHaveBeenCalled ( ) ;
170+ expect ( mockUserFindFirst ) . not . toHaveBeenCalled ( ) ;
151171 } ) ;
152- } ) ;
172+ } ) ;
173+
174+ describe ( 'Platform link routes' , ( ) => {
175+ beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
176+
177+ it ( 'should return 400 for invalid link create body' , async ( ) => {
178+ const app = await buildApp ( ) ;
179+ const res = await app . inject ( {
180+ method : 'POST' ,
181+ url : '/api/profiles/me/links' ,
182+ payload : { platform : '' , username : '' } ,
183+ } ) ;
184+
185+ expect ( res . statusCode ) . toBe ( 400 ) ;
186+ expect ( res . json ( ) . error ) . toBe ( 'Validation failed' ) ;
187+ expect ( mockPlatformLinkCreate ) . not . toHaveBeenCalled ( ) ;
188+ } ) ;
189+
190+ it ( 'should create a platform link with a valid body' , async ( ) => {
191+ const createdLink = {
192+ id : 'link-123' ,
193+ userId : 'user-123' ,
194+ platform : 'github' ,
195+ username : 'octocat' ,
196+ url : 'https://github.com/octocat' ,
197+ displayOrder : 2 ,
198+ } ;
199+
200+ mockPlatformLinkAggregate . mockResolvedValue ( { _max : { displayOrder : 1 } } ) ;
201+ mockPlatformLinkCreate . mockResolvedValue ( createdLink ) ;
202+
203+ const app = await buildApp ( ) ;
204+ const res = await app . inject ( {
205+ method : 'POST' ,
206+ url : '/api/profiles/me/links' ,
207+ payload : { platform : 'github' , username : 'octocat' } ,
208+ } ) ;
209+
210+ expect ( res . statusCode ) . toBe ( 201 ) ;
211+ expect ( res . json ( ) ) . toEqual ( createdLink ) ;
212+ expect ( mockPlatformLinkAggregate ) . toHaveBeenCalledWith ( {
213+ where : { userId : 'user-123' } ,
214+ _max : { displayOrder : true } ,
215+ } ) ;
216+ expect ( mockPlatformLinkCreate ) . toHaveBeenCalledWith ( {
217+ data : {
218+ userId : 'user-123' ,
219+ platform : 'github' ,
220+ username : 'octocat' ,
221+ url : expect . stringContaining ( 'octocat' ) ,
222+ displayOrder : 2 ,
223+ } ,
224+ } ) ;
225+ } ) ;
226+
227+ it ( 'should return 404 when updating a link that does not exist' , async ( ) => {
228+ mockPlatformLinkFindFirst . mockResolvedValue ( null ) ;
229+
230+ const app = await buildApp ( ) ;
231+ const res = await app . inject ( {
232+ method : 'PUT' ,
233+ url : '/api/profiles/me/links/link-404' ,
234+ payload : { platform : 'github' , username : 'octocat' } ,
235+ } ) ;
236+
237+ expect ( res . statusCode ) . toBe ( 404 ) ;
238+ expect ( res . json ( ) . error ) . toBe ( 'Link not found' ) ;
239+ expect ( mockPlatformLinkFindFirst ) . toHaveBeenCalledWith ( {
240+ where : { id : 'link-404' , userId : 'user-123' } ,
241+ } ) ;
242+ expect ( mockPlatformLinkUpdate ) . not . toHaveBeenCalled ( ) ;
243+ } ) ;
244+
245+ it ( 'should delete an existing platform link' , async ( ) => {
246+ mockPlatformLinkFindFirst . mockResolvedValue ( {
247+ id : 'link-123' ,
248+ userId : 'user-123' ,
249+ platform : 'github' ,
250+ username : 'octocat' ,
251+ url : 'https://github.com/octocat' ,
252+ displayOrder : 0 ,
253+ } ) ;
254+ mockPlatformLinkDelete . mockResolvedValue ( { id : 'link-123' } ) ;
255+
256+ const app = await buildApp ( ) ;
257+ const res = await app . inject ( {
258+ method : 'DELETE' ,
259+ url : '/api/profiles/me/links/link-123' ,
260+ } ) ;
261+
262+ expect ( res . statusCode ) . toBe ( 204 ) ;
263+ expect ( res . body ) . toBe ( '' ) ;
264+ expect ( mockPlatformLinkFindFirst ) . toHaveBeenCalledWith ( {
265+ where : { id : 'link-123' , userId : 'user-123' } ,
266+ } ) ;
267+ expect ( mockPlatformLinkDelete ) . toHaveBeenCalledWith ( { where : { id : 'link-123' } } ) ;
268+ } ) ;
269+ } ) ;
0 commit comments