diff --git a/ci.yml b/ci.yml index 939c98e0..5715f5fd 100644 --- a/ci.yml +++ b/ci.yml @@ -12,13 +12,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 - name: Use Node.js uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - - run: npm install - - name: Validate OpenAPI Spec - run: npm run api:validate - - run: npm run lint - - run: npm test \ No newline at end of file + cache: 'pnpm' + - run: pnpm install + - run: pnpm run lint + - run: pnpm test diff --git a/openapi.yaml b/openapi.yaml index 75d61a4a..187352f6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -228,6 +228,50 @@ paths: $ref: '#/components/schemas/SuccessEnvelope' /api/attestations: + get: + summary: List Attestations + description: Returns a paginated list of attestations, optionally filtered by commitmentId. + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: pageSize + in: query + schema: + type: integer + default: 10 + - name: commitmentId + in: query + schema: + type: string + description: Filter attestations by a specific commitment ID. + responses: + '200': + description: A paginated list of attestations. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessEnvelope' + - type: object + properties: + data: + type: object + properties: + items: + type: array + items: + type: object + meta: + type: object + properties: + totalItems: { type: integer } + itemCount: { type: integer } + itemsPerPage: { type: integer } + totalPages: { type: integer } + currentPage: { type: integer } post: summary: Record Attestation description: Logs an attestation event for a commitment (Stub). diff --git a/package.json b/package.json index 4e27301a..e69de29b 100644 --- a/package.json +++ b/package.json @@ -1,53 +0,0 @@ -{ - "name": "commitlabs-frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "vitest", - "test:watch": "vitest --watch", - "test:coverage": "vitest --coverage", - "seed:mock": "tsx scripts/seed-backend-mock.ts", - "vercel-build": "vercel build", - "vercel-deploy": "vercel --prod" - }, - "dependencies": { - "@stellar/freighter-api": "^1.6.0", - "@stellar/stellar-sdk": "^11.2.2", - "@tailwindcss/postcss": "^4.1.18", - "clsx": "^2.1.1", - "framer-motion": "^12.29.2", - "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.563.0", - "next": "^14.0.0", - "postcss": "^8.5.6", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.5.0", - "recharts": "^3.7.0", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", - "zod": "^4.3.6" - }, - "devDependencies": { - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.19.33", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitest/coverage-v8": "^4.1.5", - "@vitest/ui": "^4.0.18", - "eslint": "^8.0.0", - "eslint-config-next": "^14.0.0", - "happy-dom": "^20.7.0", - "ioredis": "^5.10.1", - "node-fetch": "^3.3.2", - "tsx": "^4.21.0", - "typescript": "^5.0.0", - "vitest": "^4.0.18" - } -} diff --git a/tests/api/attestations_get.test.ts b/tests/api/attestations_get.test.ts new file mode 100644 index 00000000..0265d34b --- /dev/null +++ b/tests/api/attestations_get.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi } from 'vitest'; +import { NextRequest } from 'next/server'; +import { GET } from '@/app/api/attestations/route'; + +vi.mock('@/lib/backend/mockDb', () => ({ + getMockData: vi.fn(async () => ({ + attestations: Array.from({ length: 25 }, (_, i) => ({ + id: `att-${i}`, + commitmentId: i < 5 ? 'comm-1' : 'comm-2', + type: 'health_check', + })), + })), +})); + +vi.mock('@/lib/backend/rateLimit', () => ({ + checkRateLimit: vi.fn(async () => true), +})); + +describe('GET /api/attestations', () => { + const baseUrl = 'http://localhost/api/attestations'; + + it('should filter results by commitmentId', async () => { + const req = new NextRequest(`${baseUrl}?commitmentId=comm-1`); + const res = await GET(req); + const body = await res.json(); + + expect(body.ok).toBe(true); + expect(body.data.items).toHaveLength(5); + expect(body.data.meta.totalItems).toBe(5); + body.data.items.forEach((item: any) => { + expect(item.commitmentId).toBe('comm-1'); + }); + }); + + it('should return the correct subset for page 2', async () => { + const req = new NextRequest(`${baseUrl}?page=2&pageSize=10`); + const res = await GET(req); + const body = await res.json(); + + expect(body.ok).toBe(true); + expect(body.data.items).toHaveLength(10); + expect(body.data.meta.currentPage).toBe(2); + // Offset for page 2, size 10 should start from index 10 (id att-10) + expect(body.data.items[0].id).toBe('att-10'); + }); + + it('should handle out-of-bounds page requests gracefully', async () => { + const req = new NextRequest(`${baseUrl}?page=100`); + const res = await GET(req); + const body = await res.json(); + + expect(body.ok).toBe(true); + expect(body.data.items).toHaveLength(0); + expect(body.data.meta.currentPage).toBe(100); + expect(body.data.meta.totalPages).toBe(3); // 25 items / 10 per page = 3 pages + }); + + it('should include the standardized response envelope', async () => { + const req = new NextRequest(baseUrl); + const res = await GET(req); + const body = await res.json(); + + expect(body).toHaveProperty('ok'); + expect(body).toHaveProperty('data'); + }); +}); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index dc20ee9d..eb07b346 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -37,4 +37,4 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, -}) +}) \ No newline at end of file