All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
749 lines
26 KiB
TypeScript
749 lines
26 KiB
TypeScript
// src/routes/versioned.test.ts
|
|
/**
|
|
* @file Unit tests for the version router factory.
|
|
* Tests ADR-008 Phase 2: API Versioning Infrastructure.
|
|
*
|
|
* These tests verify:
|
|
* - Router creation for different API versions
|
|
* - X-API-Version header on all responses
|
|
* - Deprecation headers for deprecated versions
|
|
* - Route availability filtering by version
|
|
* - Router caching behavior
|
|
* - Utility functions
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import express, { Router, Request, Response } from 'express';
|
|
import type { Logger } from 'pino';
|
|
|
|
// --- Hoisted Mock Setup ---
|
|
// vi.hoisted() is executed before imports, making values available in vi.mock factories
|
|
|
|
const { mockLoggerFn, inlineMockLogger, createMockRouterFactory } = vi.hoisted(() => {
|
|
const mockLoggerFn = vi.fn();
|
|
const inlineMockLogger = {
|
|
info: mockLoggerFn,
|
|
debug: mockLoggerFn,
|
|
error: mockLoggerFn,
|
|
warn: mockLoggerFn,
|
|
fatal: mockLoggerFn,
|
|
trace: mockLoggerFn,
|
|
silent: mockLoggerFn,
|
|
child: vi.fn().mockReturnThis(),
|
|
} as unknown as Logger;
|
|
|
|
// Factory function to create mock routers
|
|
const createMockRouterFactory = (name: string) => {
|
|
// Import express Router here since we're in hoisted context
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { Router: ExpressRouter } = require('express');
|
|
const router = ExpressRouter();
|
|
router.get('/test', (req: Request, res: Response) => {
|
|
res.json({ router: name, version: (req as { apiVersion?: string }).apiVersion });
|
|
});
|
|
return router;
|
|
};
|
|
|
|
return { mockLoggerFn, inlineMockLogger, createMockRouterFactory };
|
|
});
|
|
|
|
// --- Mock Setup ---
|
|
|
|
// Mock the logger before any imports that use it
|
|
vi.mock('../services/logger.server', () => ({
|
|
createScopedLogger: vi.fn(() => inlineMockLogger),
|
|
logger: inlineMockLogger,
|
|
}));
|
|
|
|
// Mock all domain routers with minimal test routers
|
|
// This isolates the versioned.ts tests from actual route implementations
|
|
vi.mock('./auth.routes', () => ({ default: createMockRouterFactory('auth') }));
|
|
vi.mock('./health.routes', () => ({ default: createMockRouterFactory('health') }));
|
|
vi.mock('./system.routes', () => ({ default: createMockRouterFactory('system') }));
|
|
vi.mock('./user.routes', () => ({ default: createMockRouterFactory('user') }));
|
|
vi.mock('./ai.routes', () => ({ default: createMockRouterFactory('ai') }));
|
|
vi.mock('./admin.routes', () => ({ default: createMockRouterFactory('admin') }));
|
|
vi.mock('./budget.routes', () => ({ default: createMockRouterFactory('budget') }));
|
|
vi.mock('./gamification.routes', () => ({ default: createMockRouterFactory('gamification') }));
|
|
vi.mock('./flyer.routes', () => ({ default: createMockRouterFactory('flyer') }));
|
|
vi.mock('./recipe.routes', () => ({ default: createMockRouterFactory('recipe') }));
|
|
vi.mock('./personalization.routes', () => ({
|
|
default: createMockRouterFactory('personalization'),
|
|
}));
|
|
vi.mock('./price.routes', () => ({ default: createMockRouterFactory('price') }));
|
|
vi.mock('./stats.routes', () => ({ default: createMockRouterFactory('stats') }));
|
|
vi.mock('./upc.routes', () => ({ default: createMockRouterFactory('upc') }));
|
|
vi.mock('./inventory.routes', () => ({ default: createMockRouterFactory('inventory') }));
|
|
vi.mock('./receipt.routes', () => ({ default: createMockRouterFactory('receipt') }));
|
|
vi.mock('./deals.routes', () => ({ default: createMockRouterFactory('deals') }));
|
|
vi.mock('./reactions.routes', () => ({ default: createMockRouterFactory('reactions') }));
|
|
vi.mock('./store.routes', () => ({ default: createMockRouterFactory('store') }));
|
|
vi.mock('./category.routes', () => ({ default: createMockRouterFactory('category') }));
|
|
|
|
// Import types and modules AFTER mocks are set up
|
|
import type { ApiVersion, VersionConfig } from '../config/apiVersions';
|
|
import { DEPRECATION_HEADERS } from '../middleware/deprecation.middleware';
|
|
import { errorHandler } from '../middleware/errorHandler';
|
|
|
|
// Import the module under test
|
|
import {
|
|
createVersionedRouter,
|
|
createApiRouter,
|
|
getRegisteredPaths,
|
|
getRouteByPath,
|
|
getRoutesForVersion,
|
|
clearRouterCache,
|
|
refreshRouterCache,
|
|
ROUTES,
|
|
} from './versioned';
|
|
|
|
import { API_VERSIONS, VERSION_CONFIGS } from '../config/apiVersions';
|
|
|
|
// --- Test Utilities ---
|
|
|
|
/**
|
|
* Creates a test Express app with the given router mounted at the specified path.
|
|
*/
|
|
function createTestApp(router: Router, basePath = '/api') {
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
// Inject mock logger into requests
|
|
app.use((req, res, next) => {
|
|
req.log = inlineMockLogger;
|
|
next();
|
|
});
|
|
|
|
app.use(basePath, router);
|
|
app.use(errorHandler);
|
|
return app;
|
|
}
|
|
|
|
/**
|
|
* Stores original VERSION_CONFIGS for restoration after tests that modify it.
|
|
*/
|
|
let originalVersionConfigs: Record<ApiVersion, VersionConfig>;
|
|
|
|
// --- Tests ---
|
|
|
|
describe('Versioned Router Factory', () => {
|
|
beforeAll(() => {
|
|
// Store original configs before any tests modify them
|
|
originalVersionConfigs = JSON.parse(JSON.stringify(VERSION_CONFIGS));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Clear router cache before each test to ensure fresh state
|
|
clearRouterCache();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original VERSION_CONFIGS after each test
|
|
Object.assign(VERSION_CONFIGS, originalVersionConfigs);
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Final restoration
|
|
Object.assign(VERSION_CONFIGS, originalVersionConfigs);
|
|
});
|
|
|
|
// =========================================================================
|
|
// createVersionedRouter() Tests
|
|
// =========================================================================
|
|
|
|
describe('createVersionedRouter()', () => {
|
|
describe('route registration', () => {
|
|
it('should create router with all expected routes for v1', () => {
|
|
// Act
|
|
const router = createVersionedRouter('v1');
|
|
|
|
// Assert - router should be created
|
|
expect(router).toBeDefined();
|
|
expect(typeof router).toBe('function'); // Express routers are functions
|
|
});
|
|
|
|
it('should create router with all expected routes for v2', () => {
|
|
// Act
|
|
const router = createVersionedRouter('v2');
|
|
|
|
// Assert
|
|
expect(router).toBeDefined();
|
|
expect(typeof router).toBe('function');
|
|
});
|
|
|
|
it('should register routes in the expected order', () => {
|
|
// The ROUTES array defines registration order
|
|
const expectedOrder = [
|
|
'auth',
|
|
'health',
|
|
'system',
|
|
'users',
|
|
'ai',
|
|
'admin',
|
|
'budgets',
|
|
'achievements',
|
|
'flyers',
|
|
'recipes',
|
|
'personalization',
|
|
'price-history',
|
|
'stats',
|
|
'upc',
|
|
'inventory',
|
|
'receipts',
|
|
'deals',
|
|
'reactions',
|
|
'stores',
|
|
'categories',
|
|
];
|
|
|
|
// Assert order matches
|
|
const registeredPaths = ROUTES.map((r) => r.path);
|
|
expect(registeredPaths).toEqual(expectedOrder);
|
|
});
|
|
|
|
it('should skip routes not available for specified version', () => {
|
|
// Assert - getRoutesForVersion should filter correctly
|
|
const v1Routes = getRoutesForVersion('v1');
|
|
const v2Routes = getRoutesForVersion('v2');
|
|
|
|
// All routes without versions restriction should be in both
|
|
expect(v1Routes.length).toBe(ROUTES.length);
|
|
expect(v2Routes.length).toBe(ROUTES.length);
|
|
|
|
// If we had a version-restricted route, it would only appear in that version
|
|
// This tests the filtering logic via getRoutesForVersion
|
|
expect(v1Routes.some((r) => r.path === 'auth')).toBe(true);
|
|
expect(v2Routes.some((r) => r.path === 'auth')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('X-API-Version header', () => {
|
|
it('should add X-API-Version header to all v1 responses', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBe('v1');
|
|
});
|
|
|
|
it('should add X-API-Version header to all v2 responses', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v2');
|
|
const app = createTestApp(router, '/api/v2');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v2/health/test');
|
|
|
|
// Assert
|
|
expect(response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBe('v2');
|
|
});
|
|
|
|
it('should NOT add X-API-Version header when deprecation middleware is disabled', async () => {
|
|
// Arrange - create router with deprecation headers disabled
|
|
const router = createVersionedRouter('v1', { applyDeprecationHeaders: false });
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - header should NOT be present when deprecation middleware is disabled
|
|
expect(response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('deprecation headers', () => {
|
|
it('should not add deprecation headers for active versions', async () => {
|
|
// Arrange - v1 is active by default
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - no deprecation headers
|
|
expect(response.headers[DEPRECATION_HEADERS.DEPRECATION.toLowerCase()]).toBeUndefined();
|
|
expect(response.headers[DEPRECATION_HEADERS.SUNSET.toLowerCase()]).toBeUndefined();
|
|
expect(response.headers[DEPRECATION_HEADERS.LINK.toLowerCase()]).toBeUndefined();
|
|
expect(
|
|
response.headers[DEPRECATION_HEADERS.DEPRECATION_NOTICE.toLowerCase()],
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('should add deprecation headers when version is deprecated', async () => {
|
|
// Arrange - Temporarily mark v1 as deprecated
|
|
VERSION_CONFIGS.v1 = {
|
|
version: 'v1',
|
|
status: 'deprecated',
|
|
sunsetDate: '2027-01-01T00:00:00Z',
|
|
successorVersion: 'v2',
|
|
};
|
|
|
|
// Clear cache and create fresh router with updated config
|
|
clearRouterCache();
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - deprecation headers present
|
|
expect(response.headers[DEPRECATION_HEADERS.DEPRECATION.toLowerCase()]).toBe('true');
|
|
expect(response.headers[DEPRECATION_HEADERS.SUNSET.toLowerCase()]).toBe(
|
|
'2027-01-01T00:00:00Z',
|
|
);
|
|
expect(response.headers[DEPRECATION_HEADERS.LINK.toLowerCase()]).toBe(
|
|
'</api/v2>; rel="successor-version"',
|
|
);
|
|
expect(response.headers[DEPRECATION_HEADERS.DEPRECATION_NOTICE.toLowerCase()]).toContain(
|
|
'deprecated',
|
|
);
|
|
});
|
|
|
|
it('should include sunset date in deprecation headers when provided', async () => {
|
|
// Arrange
|
|
VERSION_CONFIGS.v1 = {
|
|
version: 'v1',
|
|
status: 'deprecated',
|
|
sunsetDate: '2028-06-15T00:00:00Z',
|
|
successorVersion: 'v2',
|
|
};
|
|
|
|
clearRouterCache();
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.headers[DEPRECATION_HEADERS.SUNSET.toLowerCase()]).toBe(
|
|
'2028-06-15T00:00:00Z',
|
|
);
|
|
});
|
|
|
|
it('should include successor version link when provided', async () => {
|
|
// Arrange
|
|
VERSION_CONFIGS.v1 = {
|
|
version: 'v1',
|
|
status: 'deprecated',
|
|
successorVersion: 'v2',
|
|
};
|
|
|
|
clearRouterCache();
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.headers[DEPRECATION_HEADERS.LINK.toLowerCase()]).toBe(
|
|
'</api/v2>; rel="successor-version"',
|
|
);
|
|
});
|
|
|
|
it('should not include sunset date when not provided', async () => {
|
|
// Arrange - deprecated without sunset date
|
|
VERSION_CONFIGS.v1 = {
|
|
version: 'v1',
|
|
status: 'deprecated',
|
|
// No sunsetDate
|
|
};
|
|
|
|
clearRouterCache();
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.headers[DEPRECATION_HEADERS.DEPRECATION.toLowerCase()]).toBe('true');
|
|
expect(response.headers[DEPRECATION_HEADERS.SUNSET.toLowerCase()]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('router options', () => {
|
|
it('should apply deprecation headers middleware by default', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - X-API-Version should be set (proves middleware ran)
|
|
expect(response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBe('v1');
|
|
});
|
|
|
|
it('should skip deprecation headers middleware when disabled', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v1', { applyDeprecationHeaders: false });
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - X-API-Version should NOT be set
|
|
expect(response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// createApiRouter() Tests
|
|
// =========================================================================
|
|
|
|
describe('createApiRouter()', () => {
|
|
it('should mount all supported versions', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act & Assert - v1 should work
|
|
const v1Response = await supertest(app).get('/api/v1/health/test');
|
|
expect(v1Response.status).toBe(200);
|
|
expect(v1Response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBe('v1');
|
|
|
|
// Act & Assert - v2 should work
|
|
const v2Response = await supertest(app).get('/api/v2/health/test');
|
|
expect(v2Response.status).toBe(200);
|
|
expect(v2Response.headers[DEPRECATION_HEADERS.API_VERSION.toLowerCase()]).toBe('v2');
|
|
});
|
|
|
|
it('should return 404 for unsupported versions', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/v99/health/test');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.error.code).toBe('UNSUPPORTED_VERSION');
|
|
expect(response.body.error.message).toContain('v99');
|
|
expect(response.body.error.details.supportedVersions).toEqual(['v1', 'v2']);
|
|
});
|
|
|
|
it('should route to correct versioned router based on URL version', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act
|
|
const v1Response = await supertest(app).get('/api/v1/health/test');
|
|
const v2Response = await supertest(app).get('/api/v2/health/test');
|
|
|
|
// Assert - each response should indicate the correct version
|
|
expect(v1Response.body.version).toBe('v1');
|
|
expect(v2Response.body.version).toBe('v2');
|
|
});
|
|
|
|
it('should handle requests to various domain routers', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act & Assert - multiple domain routers
|
|
const authResponse = await supertest(app).get('/api/v1/auth/test');
|
|
expect(authResponse.status).toBe(200);
|
|
expect(authResponse.body.router).toBe('auth');
|
|
|
|
const flyerResponse = await supertest(app).get('/api/v1/flyers/test');
|
|
expect(flyerResponse.status).toBe(200);
|
|
expect(flyerResponse.body.router).toBe('flyer');
|
|
|
|
const storeResponse = await supertest(app).get('/api/v1/stores/test');
|
|
expect(storeResponse.status).toBe(200);
|
|
expect(storeResponse.body.router).toBe('store');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Utility Functions Tests
|
|
// =========================================================================
|
|
|
|
describe('getRegisteredPaths()', () => {
|
|
it('should return all registered route paths', () => {
|
|
// Act
|
|
const paths = getRegisteredPaths();
|
|
|
|
// Assert
|
|
expect(paths).toBeInstanceOf(Array);
|
|
expect(paths.length).toBe(ROUTES.length);
|
|
expect(paths).toContain('auth');
|
|
expect(paths).toContain('health');
|
|
expect(paths).toContain('flyers');
|
|
expect(paths).toContain('stores');
|
|
expect(paths).toContain('categories');
|
|
});
|
|
|
|
it('should return paths in registration order', () => {
|
|
// Act
|
|
const paths = getRegisteredPaths();
|
|
|
|
// Assert - first and last should match ROUTES order
|
|
expect(paths[0]).toBe('auth');
|
|
expect(paths[paths.length - 1]).toBe('categories');
|
|
});
|
|
});
|
|
|
|
describe('getRouteByPath()', () => {
|
|
it('should return correct registration for existing path', () => {
|
|
// Act
|
|
const authRoute = getRouteByPath('auth');
|
|
const healthRoute = getRouteByPath('health');
|
|
|
|
// Assert
|
|
expect(authRoute).toBeDefined();
|
|
expect(authRoute?.path).toBe('auth');
|
|
expect(authRoute?.description).toContain('Authentication');
|
|
|
|
expect(healthRoute).toBeDefined();
|
|
expect(healthRoute?.path).toBe('health');
|
|
expect(healthRoute?.description).toContain('Health');
|
|
});
|
|
|
|
it('should return undefined for non-existent path', () => {
|
|
// Act
|
|
const result = getRouteByPath('non-existent-path');
|
|
|
|
// Assert
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return undefined for empty string', () => {
|
|
// Act
|
|
const result = getRouteByPath('');
|
|
|
|
// Assert
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('getRoutesForVersion()', () => {
|
|
it('should return all routes when no version restrictions exist', () => {
|
|
// Act
|
|
const v1Routes = getRoutesForVersion('v1');
|
|
const v2Routes = getRoutesForVersion('v2');
|
|
|
|
// Assert - all routes should be available in both versions
|
|
// (since none of the default routes have version restrictions)
|
|
expect(v1Routes.length).toBe(ROUTES.length);
|
|
expect(v2Routes.length).toBe(ROUTES.length);
|
|
});
|
|
|
|
it('should filter routes based on version restrictions', () => {
|
|
// This tests the filtering logic - routes with versions array
|
|
// should only appear for versions listed in that array
|
|
const v1Routes = getRoutesForVersion('v1');
|
|
|
|
// All routes should have path and router properties
|
|
v1Routes.forEach((route) => {
|
|
expect(route.path).toBeDefined();
|
|
expect(route.router).toBeDefined();
|
|
expect(route.description).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it('should include routes without version restrictions in all versions', () => {
|
|
// Routes without versions array should appear in all versions
|
|
const authRoute = ROUTES.find((r) => r.path === 'auth');
|
|
expect(authRoute?.versions).toBeUndefined(); // No version restriction
|
|
|
|
const v1Routes = getRoutesForVersion('v1');
|
|
const v2Routes = getRoutesForVersion('v2');
|
|
|
|
expect(v1Routes.some((r) => r.path === 'auth')).toBe(true);
|
|
expect(v2Routes.some((r) => r.path === 'auth')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Router Cache Tests
|
|
// =========================================================================
|
|
|
|
describe('router cache', () => {
|
|
it('should cache routers after creation', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act - make multiple requests (should use cached router)
|
|
const response1 = await supertest(app).get('/api/v1/health/test');
|
|
const response2 = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert - both requests should succeed (proving cache works)
|
|
expect(response1.status).toBe(200);
|
|
expect(response2.status).toBe(200);
|
|
});
|
|
|
|
it('should clear cache with clearRouterCache()', () => {
|
|
// Arrange - create some routers to populate cache
|
|
createVersionedRouter('v1');
|
|
createVersionedRouter('v2');
|
|
|
|
// Act
|
|
clearRouterCache();
|
|
|
|
// Assert - cache should be cleared (logger would have logged)
|
|
expect(mockLoggerFn).toHaveBeenCalledWith('Versioned router cache cleared');
|
|
});
|
|
|
|
it('should refresh cache with refreshRouterCache()', () => {
|
|
// Arrange
|
|
createVersionedRouter('v1');
|
|
|
|
// Act
|
|
refreshRouterCache();
|
|
|
|
// Assert - cache should be refreshed (logger would have logged)
|
|
expect(mockLoggerFn).toHaveBeenCalledWith(
|
|
expect.objectContaining({ cachedVersions: expect.any(Array) }),
|
|
'Versioned router cache refreshed',
|
|
);
|
|
});
|
|
|
|
it('should create routers on-demand if not in cache', async () => {
|
|
// Arrange
|
|
clearRouterCache();
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act - request should trigger on-demand router creation
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// ROUTES Configuration Tests
|
|
// =========================================================================
|
|
|
|
describe('ROUTES configuration', () => {
|
|
it('should have all required properties for each route', () => {
|
|
ROUTES.forEach((route) => {
|
|
expect(route.path).toBeDefined();
|
|
expect(typeof route.path).toBe('string');
|
|
expect(route.path.length).toBeGreaterThan(0);
|
|
|
|
expect(route.router).toBeDefined();
|
|
expect(typeof route.router).toBe('function');
|
|
|
|
expect(route.description).toBeDefined();
|
|
expect(typeof route.description).toBe('string');
|
|
expect(route.description.length).toBeGreaterThan(0);
|
|
|
|
// versions is optional
|
|
if (route.versions !== undefined) {
|
|
expect(Array.isArray(route.versions)).toBe(true);
|
|
route.versions.forEach((v) => {
|
|
expect(API_VERSIONS).toContain(v);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should not have duplicate paths', () => {
|
|
const paths = ROUTES.map((r) => r.path);
|
|
const uniquePaths = new Set(paths);
|
|
expect(uniquePaths.size).toBe(paths.length);
|
|
});
|
|
|
|
it('should have expected number of routes', () => {
|
|
// This ensures we don't accidentally remove routes
|
|
expect(ROUTES.length).toBe(20);
|
|
});
|
|
|
|
it('should include all core domain routers', () => {
|
|
const paths = getRegisteredPaths();
|
|
const expectedRoutes = [
|
|
'auth',
|
|
'health',
|
|
'system',
|
|
'users',
|
|
'ai',
|
|
'admin',
|
|
'budgets',
|
|
'achievements',
|
|
'flyers',
|
|
'recipes',
|
|
'personalization',
|
|
'price-history',
|
|
'stats',
|
|
'upc',
|
|
'inventory',
|
|
'receipts',
|
|
'deals',
|
|
'reactions',
|
|
'stores',
|
|
'categories',
|
|
];
|
|
|
|
expectedRoutes.forEach((route) => {
|
|
expect(paths).toContain(route);
|
|
});
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Edge Cases and Error Handling
|
|
// =========================================================================
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle requests to nested routes', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act - request to nested path
|
|
const response = await supertest(app).get('/api/v1/health/test');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.router).toBe('health');
|
|
});
|
|
|
|
it('should return 404 for non-existent routes within valid version', async () => {
|
|
// Arrange
|
|
const router = createVersionedRouter('v1');
|
|
const app = createTestApp(router, '/api/v1');
|
|
|
|
// Act - request to non-existent domain
|
|
const response = await supertest(app).get('/api/v1/nonexistent/endpoint');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should handle multiple concurrent requests', async () => {
|
|
// Arrange
|
|
const apiRouter = createApiRouter();
|
|
const app = createTestApp(apiRouter, '/api');
|
|
|
|
// Act - make concurrent requests
|
|
const requests = [
|
|
supertest(app).get('/api/v1/health/test'),
|
|
supertest(app).get('/api/v1/auth/test'),
|
|
supertest(app).get('/api/v2/health/test'),
|
|
supertest(app).get('/api/v2/flyers/test'),
|
|
];
|
|
|
|
const responses = await Promise.all(requests);
|
|
|
|
// Assert - all should succeed
|
|
responses.forEach((response) => {
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
});
|
|
});
|