Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, {
Expand Down
28 changes: 28 additions & 0 deletions backend/src/routes/api.ts
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 2 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
46 changes: 46 additions & 0 deletions backend/src/routes/lottery.routes.ts
Original file line number Diff line number Diff line change
@@ -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 };
66 changes: 66 additions & 0 deletions backend/tests/lottery.routes.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});