Files
flyer-crawler.projectium.com/src/middleware/apiVersion.middleware.test.ts
Torben Sorensen f10c6c0cd6
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
Complete ADR-008 Phase 2
2026-01-27 11:06:09 -08:00

401 lines
12 KiB
TypeScript

// src/middleware/apiVersion.middleware.test.ts
/**
* @file Unit tests for API version detection middleware (ADR-008 Phase 2).
* @see src/middleware/apiVersion.middleware.ts
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import type { Request, Response, NextFunction } from 'express';
import {
detectApiVersion,
extractApiVersionFromPath,
hasApiVersion,
getRequestApiVersion,
VERSION_ERROR_CODES,
} from './apiVersion.middleware';
import { DEFAULT_VERSION, SUPPORTED_VERSIONS } from '../config/apiVersions';
import { createMockRequest } from '../tests/utils/createMockRequest';
import { createMockLogger } from '../tests/utils/mockLogger';
describe('apiVersion.middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: NextFunction & Mock;
let mockJson: Mock;
let mockStatus: Mock;
beforeEach(() => {
// Reset mocks before each test
mockJson = vi.fn().mockReturnThis();
mockStatus = vi.fn().mockReturnValue({ json: mockJson });
mockNext = vi.fn();
mockResponse = {
status: mockStatus,
json: mockJson,
};
});
describe('detectApiVersion', () => {
it('should extract v1 from req.params.version and attach to req.apiVersion', () => {
// Arrange
mockRequest = createMockRequest({
params: { version: 'v1' },
path: '/users',
method: 'GET',
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockNext).toHaveBeenCalledWith();
expect(mockRequest.apiVersion).toBe('v1');
expect(mockRequest.versionDeprecation).toBeDefined();
expect(mockRequest.versionDeprecation?.deprecated).toBe(false);
});
it('should extract v2 from req.params.version and attach to req.apiVersion', () => {
// Arrange
mockRequest = createMockRequest({
params: { version: 'v2' },
path: '/flyers',
method: 'POST',
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe('v2');
expect(mockRequest.versionDeprecation).toBeDefined();
});
it('should default to v1 when no version parameter is present', () => {
// Arrange
mockRequest = createMockRequest({
params: {},
path: '/users',
method: 'GET',
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe(DEFAULT_VERSION);
expect(mockRequest.versionDeprecation).toBeDefined();
});
it('should return 404 with UNSUPPORTED_VERSION for invalid version v99', () => {
// Arrange
mockRequest = createMockRequest({
params: { version: 'v99' },
path: '/users',
method: 'GET',
ip: '127.0.0.1',
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).not.toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(404);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.objectContaining({
code: VERSION_ERROR_CODES.UNSUPPORTED_VERSION,
message: expect.stringContaining("API version 'v99' is not supported"),
details: expect.objectContaining({
requestedVersion: 'v99',
supportedVersions: expect.arrayContaining(['v1', 'v2']),
}),
}),
}),
);
});
it('should return 404 for non-versioned format like "latest"', () => {
// Arrange
mockRequest = createMockRequest({
params: { version: 'latest' },
path: '/users',
method: 'GET',
ip: '192.168.1.1',
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).not.toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(404);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.objectContaining({
code: VERSION_ERROR_CODES.UNSUPPORTED_VERSION,
message: expect.stringContaining("API version 'latest' is not supported"),
}),
}),
);
});
it('should log warning when invalid version is requested', () => {
// Arrange
const childLogger = createMockLogger();
const mockLog = createMockLogger();
vi.mocked(mockLog.child).mockReturnValue(
childLogger as unknown as ReturnType<typeof mockLog.child>,
);
mockRequest = createMockRequest({
params: { version: 'v999' },
path: '/test',
method: 'GET',
ip: '10.0.0.1',
log: mockLog,
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockLog.child).toHaveBeenCalledWith({ middleware: 'detectApiVersion' });
expect(childLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({
attemptedVersion: 'v999',
supportedVersions: SUPPORTED_VERSIONS,
}),
'Invalid API version requested',
);
});
it('should log debug when valid version is detected', () => {
// Arrange
const childLogger = createMockLogger();
const mockLog = createMockLogger();
vi.mocked(mockLog.child).mockReturnValue(
childLogger as unknown as ReturnType<typeof mockLog.child>,
);
mockRequest = createMockRequest({
params: { version: 'v1' },
path: '/users',
method: 'GET',
log: mockLog,
});
// Act
detectApiVersion(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(childLogger.debug).toHaveBeenCalledWith(
{ apiVersion: 'v1' },
'API version detected from URL',
);
});
});
describe('extractApiVersionFromPath', () => {
it('should extract v1 from /v1/users path', () => {
// Arrange
mockRequest = createMockRequest({
path: '/v1/users',
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe('v1');
expect(mockRequest.versionDeprecation).toBeDefined();
});
it('should extract v2 from /v2/flyers/123 path', () => {
// Arrange
mockRequest = createMockRequest({
path: '/v2/flyers/123',
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe('v2');
});
it('should default to v1 for unversioned paths', () => {
// Arrange
mockRequest = createMockRequest({
path: '/users',
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe(DEFAULT_VERSION);
});
it('should default to v1 for paths without leading slash', () => {
// Arrange
mockRequest = createMockRequest({
path: 'v1/users', // No leading slash - won't match regex
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe(DEFAULT_VERSION);
});
it('should use default for unsupported version numbers in path', () => {
// Arrange
const childLogger = createMockLogger();
const mockLog = createMockLogger();
vi.mocked(mockLog.child).mockReturnValue(
childLogger as unknown as ReturnType<typeof mockLog.child>,
);
mockRequest = createMockRequest({
path: '/v99/users',
params: {},
log: mockLog,
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe(DEFAULT_VERSION);
expect(childLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({
attemptedVersion: 'v99',
supportedVersions: SUPPORTED_VERSIONS,
}),
'Unsupported API version in path, falling back to default',
);
});
it('should handle paths with only version segment', () => {
// Arrange: Path like "/v1/" (just version, no resource)
mockRequest = createMockRequest({
path: '/v1/',
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe('v1');
});
it('should NOT extract version from path like /users/v1 (not at start)', () => {
// Arrange: Version appears later in path, not at the start
mockRequest = createMockRequest({
path: '/users/v1/profile',
params: {},
});
// Act
extractApiVersionFromPath(mockRequest as Request, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockRequest.apiVersion).toBe(DEFAULT_VERSION);
});
});
describe('hasApiVersion', () => {
it('should return true when apiVersion is set to valid version', () => {
// Arrange
mockRequest = createMockRequest({});
mockRequest.apiVersion = 'v1';
// Act & Assert
expect(hasApiVersion(mockRequest as Request)).toBe(true);
});
it('should return false when apiVersion is undefined', () => {
// Arrange
mockRequest = createMockRequest({});
mockRequest.apiVersion = undefined;
// Act & Assert
expect(hasApiVersion(mockRequest as Request)).toBe(false);
});
it('should return false when apiVersion is invalid', () => {
// Arrange
mockRequest = createMockRequest({});
// Force an invalid version (bypassing TypeScript) - eslint-disable-next-line
(mockRequest as unknown as { apiVersion: string }).apiVersion = 'v99';
// Act & Assert
expect(hasApiVersion(mockRequest as Request)).toBe(false);
});
});
describe('getRequestApiVersion', () => {
it('should return the request apiVersion when set', () => {
// Arrange
mockRequest = createMockRequest({});
mockRequest.apiVersion = 'v2';
// Act
const version = getRequestApiVersion(mockRequest as Request);
// Assert
expect(version).toBe('v2');
});
it('should return DEFAULT_VERSION when apiVersion is undefined', () => {
// Arrange
mockRequest = createMockRequest({});
mockRequest.apiVersion = undefined;
// Act
const version = getRequestApiVersion(mockRequest as Request);
// Assert
expect(version).toBe(DEFAULT_VERSION);
});
it('should return DEFAULT_VERSION when apiVersion is invalid', () => {
// Arrange
mockRequest = createMockRequest({});
// Force an invalid version - eslint-disable-next-line
(mockRequest as unknown as { apiVersion: string }).apiVersion = 'invalid';
// Act
const version = getRequestApiVersion(mockRequest as Request);
// Assert
expect(version).toBe(DEFAULT_VERSION);
});
});
describe('VERSION_ERROR_CODES', () => {
it('should have UNSUPPORTED_VERSION error code', () => {
expect(VERSION_ERROR_CODES.UNSUPPORTED_VERSION).toBe('UNSUPPORTED_VERSION');
});
});
});