From 2be31b3e623cbc0048c8cda46cb07477c81dd416 Mon Sep 17 00:00:00 2001 From: Jiggy Date: Wed, 17 Jun 2026 20:40:38 +0100 Subject: [PATCH 1/2] fix: mount v1Router to enable /api/v1/* routes - Import and mount v1Router at /api/v1 (canonical namespace) - Add legacyApiDeprecation middleware to /api routes - Fixes 404 errors on all /api/v1/* endpoints Closes #9 --- backend/src/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 0f4cf1c..2e076eb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,7 +4,9 @@ import cors from 'cors' import { createServer } from 'node:http' import { WebSocketServer } from 'ws' import { portfolioRouter } from './api/routes.js' +import { v1Router } from './api/v1Router.js' import { errorHandler, notFound } from './middleware/errorHandler.js' +import { legacyApiDeprecation } from './middleware/legacyApiDeprecation.js' import { globalRateLimiter } from './middleware/rateLimit.js' import { RebalancingService } from './monitoring/rebalancer.js' import { AutoRebalancerService } from './services/autoRebalancer.js' @@ -174,7 +176,13 @@ app.get('/', (req, res) => { }) // Mount API routes -app.use('/api', portfolioRouter) +// Canonical v1 namespace +app.use('/api/v1', v1Router) + +// Legacy namespace with deprecation headers +app.use('/api', legacyApiDeprecation, portfolioRouter) + +// Root namespace (kept for backward compatibility) app.use('/', portfolioRouter) // 404 handler From cc7578631b87b338c054c6b167b07c790dc53a9f Mon Sep 17 00:00:00 2001 From: Jiggy Date: Thu, 18 Jun 2026 15:03:44 +0100 Subject: [PATCH 2/2] Address review: Remove root mount and add v1 namespace tests - Remove app.use('/', portfolioRouter) to prevent unintentional root-level exposure - Add integration tests for /api/v1/* (canonical) and /api/* (legacy) - Tests verify both namespaces work and deprecation headers are present --- backend/src/index.ts | 3 -- backend/src/test/api.integration.test.ts | 58 +++++++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 2e076eb..c80b57a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -182,9 +182,6 @@ app.use('/api/v1', v1Router) // Legacy namespace with deprecation headers app.use('/api', legacyApiDeprecation, portfolioRouter) -// Root namespace (kept for backward compatibility) -app.use('/', portfolioRouter) - // 404 handler app.use((req, res) => { console.log(`404 - Route not found: ${req.method} ${req.url}`) diff --git a/backend/src/test/api.integration.test.ts b/backend/src/test/api.integration.test.ts index 6783816..1fdab13 100644 --- a/backend/src/test/api.integration.test.ts +++ b/backend/src/test/api.integration.test.ts @@ -4,6 +4,8 @@ import cors from 'cors' import request from 'supertest' import { Keypair } from '@stellar/stellar-sdk' import { portfolioRouter } from '../api/routes.js' +import { v1Router } from '../api/v1Router.js' +import { legacyApiDeprecation } from '../middleware/legacyApiDeprecation.js' import { mkdirSync, rmSync, existsSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' @@ -41,7 +43,9 @@ beforeAll(async () => { app.set('trust proxy', 1) - app.use('/api', portfolioRouter) + // Mount v1 (canonical) and legacy API routes + app.use('/api/v1', v1Router) + app.use('/api', legacyApiDeprecation, portfolioRouter) app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('API Error:', err) @@ -407,3 +411,55 @@ describe('Notifications - userId must be a valid Stellar public key', () => { }) }) +// ─── v1 API Namespace Tests ───────────────────────────────────────────────── + +describe('API Namespace - /api/v1 (canonical) vs /api (legacy)', () => { + it('GET /api/v1/prices returns 200 (canonical namespace)', async () => { + const response = await request(app) + .get('/api/v1/prices') + .expect(200) + + expect(Object.keys(response.body).length).toBeGreaterThan(0) + }) + + it('GET /api/prices returns 200 with deprecation headers (legacy namespace)', async () => { + const response = await request(app) + .get('/api/prices') + .expect(200) + + // Check for deprecation headers + expect(response.headers['deprecation']).toBe('true') + expect(response.headers['sunset']).toBeDefined() + expect(response.headers['link']).toContain('deprecation') + + // Data should still work + expect(Object.keys(response.body).length).toBeGreaterThan(0) + }) + + it('/api/v1/* and /api/* return same data structure', async () => { + const v1Response = await request(app) + .get('/api/v1/prices') + .expect(200) + + const legacyResponse = await request(app) + .get('/api/prices') + .expect(200) + + // Both should return price data with same structure + expect(Object.keys(v1Response.body).length).toBeGreaterThan(0) + expect(Object.keys(legacyResponse.body).length).toBeGreaterThan(0) + + // Structure should match + const v1Keys = Object.keys(v1Response.body).sort() + const legacyKeys = Object.keys(legacyResponse.body).sort() + expect(v1Keys).toEqual(legacyKeys) + }) + + it('root-level routes are not exposed (no /prices without prefix)', async () => { + const response = await request(app) + .get('/prices') + .expect((res) => { + expect(res.status).toBe(404) + }) + }) +})