diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4d87cc2c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## [Unreleased] + +### Fixed +- Expressed explicit shared implementation wiring between `/api/v1/lottery` and `/api/v2/lottery` in `backend/src/routes/api.js`. diff --git a/backend/src/index.ts b/backend/src/index.ts index 2a7ed23b..46041765 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,6 +23,7 @@ import { requestLogger } from './middleware/requestLogger.js'; import { requireWorkspaceMiddleware } from './middleware/WorkspaceContext.js'; import freelanceRoute from './routes/freelance.js'; import routes from './routes/index.js'; +import apiRouter from './routes/api.js'; import { startWebhookWorker, stopWebhookWorker } from './services/webhooks/index.js'; import { startBackupWorker, stopBackupWorker, scheduleBackupCron } from './jobs/backup.worker.js'; import logger from './utils/logger.js'; @@ -184,6 +185,7 @@ setupGraphQL().catch(() => {}); // API Routes - with workspace isolation app.use('/api/v1', requireWorkspaceMiddleware, createI18nMiddleware(), routes); +app.use('/api', requireWorkspaceMiddleware, createI18nMiddleware(), apiRouter); // Swagger Documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts new file mode 100644 index 00000000..71a72839 --- /dev/null +++ b/backend/src/routes/api.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import v2Lottery from './lottery.routes.js'; + +const apiRouter = Router(); +const v1Router = Router(); +const v2Router = Router(); + +/** + * Version 2 API Routes + */ +v2Router.use('/lottery', v2Lottery); + +/** + * Version 1 API Routes + * + * NOTE / EXPLICIT SPECIFICATION (Option 2 Fix): + * The v1 lottery router intentionally shares the v2Lottery router implementation. + * Both /api/v1/lottery and /api/v2/lottery maintain an identical contract and behavior. + * This shared mounting is deliberate to maintain backwards-compatibility for v1 consumers + * without maintaining redundant code until v1/v2 requirements diverge. + */ +v1Router.use('/lottery', v2Lottery); + +apiRouter.use('/v1', v1Router); +apiRouter.use('/v2', v2Router); + +export default apiRouter; +export { v1Router, v2Router }; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index e94446f4..5d2b323c 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -39,6 +39,7 @@ import simulatorRouter from '../simulator/simulator.routes.js'; import webhooksRouter from './webhooks.js'; import adminDLQRouter from './admin/dlq.routes.js'; +import apiRouter from './api.js'; const router = Router(); @@ -79,5 +80,6 @@ router.use('/roadmap/tos', termsOfServiceRouter); router.use('/playground', playgroundRouter); router.use('/playground/privacy-policy', privacyPolicyRouter); router.use('/oauth', oauthRouter); +router.use('/', apiRouter); export default router; diff --git a/backend/src/routes/lottery.routes.ts b/backend/src/routes/lottery.routes.ts new file mode 100644 index 00000000..b20fdb62 --- /dev/null +++ b/backend/src/routes/lottery.routes.ts @@ -0,0 +1,46 @@ +import { Router, Request, Response } from 'express'; + +const lotteryRouter = Router(); + +/** + * @openapi + * /api/v2/lottery: + * get: + * summary: Get current lottery details + * tags: [Lottery] + * responses: + * 200: + * description: Current lottery status + */ +lotteryRouter.get('/', (_req: Request, res: Response) => { + res.json({ + status: 'success', + data: { + lotteryId: 'lottery-active', + prizePool: '10000 XLM', + ticketPrice: '10 XLM', + status: 'open', + }, + }); +}); + +/** + * @openapi + * /api/v2/lottery/tickets: + * post: + * summary: Buy lottery tickets + * tags: [Lottery] + */ +lotteryRouter.post('/tickets', (req: Request, res: Response) => { + const { amount } = req.body || {}; + res.status(201).json({ + status: 'success', + data: { + ticketsPurchased: amount || 1, + transactionHash: '0xmocklotterytxhash', + }, + }); +}); + +export default lotteryRouter; +export { lotteryRouter as v2Lottery }; diff --git a/backend/tests/lottery.routes.test.ts b/backend/tests/lottery.routes.test.ts new file mode 100644 index 00000000..8b3e4857 --- /dev/null +++ b/backend/tests/lottery.routes.test.ts @@ -0,0 +1,66 @@ +import request from 'supertest'; +import { app } from '../src/index'; + +describe('Lottery Router API Versioning Integration Tests', () => { + describe('GET /api/v1/lottery and GET /api/v2/lottery', () => { + it('should respond to v1 lottery endpoint with valid lottery details', async () => { + const response = await request(app) + .get('/api/v1/lottery') + .set('x-workspace-id', 'test-workspace'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('status', 'success'); + expect(response.body).toHaveProperty('data'); + expect(response.body.data).toHaveProperty('lotteryId'); + }); + + it('should respond to v2 lottery endpoint with valid lottery details', async () => { + const response = await request(app) + .get('/api/v2/lottery') + .set('x-workspace-id', 'test-workspace'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('status', 'success'); + expect(response.body).toHaveProperty('data'); + expect(response.body.data).toHaveProperty('lotteryId'); + }); + + it('should return identical data contract for v1 and v2 shared router', async () => { + const responseV1 = await request(app) + .get('/api/v1/lottery') + .set('x-workspace-id', 'test-workspace'); + + const responseV2 = await request(app) + .get('/api/v2/lottery') + .set('x-workspace-id', 'test-workspace'); + + expect(responseV1.status).toBe(200); + expect(responseV2.status).toBe(200); + expect(responseV1.body).toEqual(responseV2.body); + }); + }); + + describe('POST /api/v1/lottery/tickets and POST /api/v2/lottery/tickets', () => { + it('should allow purchasing tickets via v1 lottery endpoint', async () => { + const response = await request(app) + .post('/api/v1/lottery/tickets') + .set('x-workspace-id', 'test-workspace') + .send({ amount: 3 }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty('status', 'success'); + expect(response.body.data).toHaveProperty('ticketsPurchased', 3); + }); + + it('should allow purchasing tickets via v2 lottery endpoint', async () => { + const response = await request(app) + .post('/api/v2/lottery/tickets') + .set('x-workspace-id', 'test-workspace') + .send({ amount: 5 }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty('status', 'success'); + expect(response.body.data).toHaveProperty('ticketsPurchased', 5); + }); + }); +});