Skip to content

Commit ddf42b0

Browse files
authored
Add profile platform link route tests (#238)
* feat: address #5 backend: add coverage for platform link features (POST/PUT/DELETE /me/links) * test: preserve profile regression coverage after rebase * test: assert rejected profile updates skip writes
1 parent b0a6223 commit ddf42b0

1 file changed

Lines changed: 136 additions & 19 deletions

File tree

‎apps/backend/src/__tests__/profiles.test.ts‎

Lines changed: 136 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1+
import { Prisma, type PrismaClient } from '@prisma/client';
12
import Fastify, { type FastifyInstance } from 'fastify';
23
import { describe, it, expect, beforeEach, vi } from 'vitest';
34

45
import { profileRoutes } from '../routes/profiles.js';
56

6-
import type { PrismaClient } from '@prisma/client';
7-
87
const 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+
2533
const 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

Comments
 (0)