diff --git a/packages/api/src/__tests__/bounties_list.test.ts b/packages/api/src/__tests__/bounties_list.test.ts index 7030900..cac4126 100644 --- a/packages/api/src/__tests__/bounties_list.test.ts +++ b/packages/api/src/__tests__/bounties_list.test.ts @@ -14,8 +14,13 @@ vi.mock('../db', () => ({ query: { bounties: { findMany: vi.fn(), + findFirst: vi.fn(), + }, + users: { + findFirst: vi.fn(), }, }, + select: vi.fn(), }, })); @@ -161,4 +166,99 @@ describe('GET /api/bounties', () => { const body = await res.json(); expect(body.error).toBe('Invalid cursor'); }); + + it('should return detailed bounty information for GET /api/bounties/:id', async () => { + const bountyId = 'bounty-1'; + vi.mocked(db.query.bounties.findFirst).mockResolvedValue({ + id: bountyId, + title: 'Bounty 1', + status: 'open', + creatorId: 'creator-1', + assigneeId: 'assignee-1', + } as any); + + vi.mocked(db.query.users.findFirst) + .mockResolvedValueOnce({ username: 'creatorUser', avatarUrl: 'https://avatar/creator.png' } as any) + .mockResolvedValueOnce({ username: 'assigneeUser', avatarUrl: 'https://avatar/assignee.png' } as any); + + vi.mocked(db.select as any).mockReturnValue({ + from: () => ({ + where: async () => [{ count: 3 }], + }), + } as any); + + const res = await app.request(`/api/bounties/${bountyId}`, { + headers: { + 'Authorization': 'Bearer valid.token' + } + }); + + expect(res.status).toBe(200); + const body = await res.json(); + + expect(body.id).toBe(bountyId); + expect(body.creator).toEqual({ + username: 'creatorUser', + avatar: 'https://avatar/creator.png', + }); + expect(body.assignee).toEqual({ + username: 'assigneeUser', + avatar: 'https://avatar/assignee.png', + }); + expect(body.applicationCount).toBe(3); + }); + + + it('should return null assignee when bounty has no assignee for GET /api/bounties/:id', async () => { + const bountyId = 'bounty-no-assignee'; + + vi.mocked(db.query.bounties.findFirst).mockResolvedValue({ + id: bountyId, + title: 'Bounty 2', + status: 'open', + creatorId: 'creator-1', + assigneeId: null, + } as any); + + vi.mocked(db.query.users.findFirst) + .mockResolvedValueOnce({ username: 'creatorUser', avatarUrl: 'https://avatar/creator.png' } as any); + + vi.mocked(db.select as any).mockReturnValue({ + from: () => ({ + where: async () => [{ count: 1 }], + }), + } as any); + + const res = await app.request(`/api/bounties/${bountyId}`, { + headers: { + 'Authorization': 'Bearer valid.token' + } + }); + + expect(res.status).toBe(200); + const body = await res.json(); + + expect(body.id).toBe(bountyId); + expect(body.creator).toEqual({ + username: 'creatorUser', + avatar: 'https://avatar/creator.png', + }); + expect(body.assignee).toBeNull(); + expect(body.applicationCount).toBe(1); + }); + + it('should return 404 when bounty is not found for GET /api/bounties/:id', async () => { + vi.mocked(db.query.bounties.findFirst).mockResolvedValue(null as any); + + const res = await app.request('/api/bounties/non-existent', { + headers: { + 'Authorization': 'Bearer valid.token' + } + }); + + expect(res.status).toBe(404); + const body = await res.json(); + expect(body.error).toBe('Bounty not found'); + }); + }); diff --git a/packages/api/src/routes/bounties.ts b/packages/api/src/routes/bounties.ts index 2f4f6e6..da6809c 100644 --- a/packages/api/src/routes/bounties.ts +++ b/packages/api/src/routes/bounties.ts @@ -2,7 +2,7 @@ import { Hono } from 'hono'; import { Variables } from '../middleware/auth'; import { ensureBountyCreator, ensureBountyAssignee } from '../middleware/resource-auth'; import { db } from '../db'; -import { bounties } from '../db/schema'; +import { bounties, users, applications } from '../db/schema'; import { eq, and, gte, lte, sql, desc, or, lt } from 'drizzle-orm'; const bountiesRouter = new Hono<{ Variables: Variables }>(); @@ -136,7 +136,45 @@ bountiesRouter.get('/:id', async (c) => { return c.json({ error: 'Bounty not found' }, 404); } - return c.json(bounty); + const [creator, assignee, applicationCount] = await Promise.all([ + db.query.users.findFirst({ + where: eq(users.id, bounty.creatorId), + columns: { + username: true, + avatarUrl: true, + }, + }), + bounty.assigneeId + ? db.query.users.findFirst({ + where: eq(users.id, bounty.assigneeId), + columns: { + username: true, + avatarUrl: true, + }, + }) + : Promise.resolve(null), + db + .select({ count: sql`count(*)` }) + .from(applications) + .where(eq(applications.bountyId, id)), + ]); + + return c.json({ + ...bounty, + creator: creator + ? { + username: creator.username, + avatar: creator.avatarUrl, + } + : null, + assignee: assignee + ? { + username: assignee.username, + avatar: assignee.avatarUrl, + } + : null, + applicationCount: Number(applicationCount[0]?.count ?? 0), + }); }); /**