diff --git a/packages/api/src/__tests__/bounties_detail.test.ts b/packages/api/src/__tests__/bounties_detail.test.ts new file mode 100644 index 0000000..c41437a --- /dev/null +++ b/packages/api/src/__tests__/bounties_detail.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { createApp } from '../app'; +import { verify } from 'hono/jwt'; +import { db } from '../db'; + +vi.mock('hono/jwt', () => ({ + verify: vi.fn(), +})); + +vi.mock('../db', () => ({ + db: { + query: { + bounties: { + findFirst: vi.fn(), + }, + applications: { + findMany: vi.fn(), + }, + users: { + findFirst: vi.fn(), + }, + }, + }, +})); + +describe('GET /api/bounties/:id', () => { + let app: ReturnType; + + beforeAll(() => { + app = createApp(); + vi.mocked(verify).mockResolvedValue({ + sub: 'test-user-id', + username: 'testuser', + exp: Math.floor(Date.now() / 1000) + 3600, + }); + process.env.JWT_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\\nfake\\n-----END PUBLIC KEY-----'; + }); + + it('returns bounty detail with creator, assignee and application_count', async () => { + vi.mocked(db.query.bounties.findFirst).mockResolvedValue({ + id: 'bounty-1', + title: 'Fix endpoint', + status: 'assigned', + creatorId: 'creator-1', + assigneeId: 'assignee-1', + createdAt: new Date('2025-01-01T00:00:00Z'), + updatedAt: new Date('2025-01-02T00:00:00Z'), + } as any); + + vi.mocked(db.query.applications.findMany).mockResolvedValue([ + { id: 'app-1' }, + { id: 'app-2' }, + ] as any); + + vi.mocked(db.query.users.findFirst) + .mockResolvedValueOnce({ id: 'creator-1', username: 'alice', avatarUrl: 'https://avatar/alice.png' } as any) + .mockResolvedValueOnce({ id: 'assignee-1', username: 'bob', avatarUrl: 'https://avatar/bob.png' } as any); + + const res = await app.request('/api/bounties/bounty-1', { + headers: { Authorization: 'Bearer valid.token' }, + }); + + expect(res.status).toBe(200); + const body = await res.json(); + + expect(body.id).toBe('bounty-1'); + expect(body.status).toBe('assigned'); + expect(body.application_count).toBe(2); + expect(body.creator).toEqual({ username: 'alice', avatar_url: 'https://avatar/alice.png' }); + expect(body.assignee).toEqual({ username: 'bob', avatar_url: 'https://avatar/bob.png' }); + }); + + it('returns 404 when bounty does not exist', async () => { + vi.mocked(db.query.bounties.findFirst).mockResolvedValue(null as any); + + const res = await app.request('/api/bounties/missing', { + 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..57992ad 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 { applications, bounties, users } from '../db/schema'; import { eq, and, gte, lte, sql, desc, or, lt } from 'drizzle-orm'; const bountiesRouter = new Hono<{ Variables: Variables }>(); @@ -136,7 +136,39 @@ bountiesRouter.get('/:id', async (c) => { return c.json({ error: 'Bounty not found' }, 404); } - return c.json(bounty); + const [applicationsForBounty, creator, assignee] = await Promise.all([ + db.query.applications.findMany({ + where: eq(applications.bountyId, bounty.id), + columns: { id: true }, + }), + 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), + ]); + + return c.json({ + ...bounty, + application_count: applicationsForBounty.length, + creator: creator + ? { + username: creator.username, + avatar_url: creator.avatarUrl, + } + : null, + assignee: assignee + ? { + username: assignee.username, + avatar_url: assignee.avatarUrl, + } + : null, + }); }); /**