Files
flyer-crawler.projectium.com/src/middleware/deprecation.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

451 lines
14 KiB
TypeScript

// src/middleware/deprecation.middleware.test.ts
/**
* @file Unit tests for deprecation header middleware.
* Tests RFC 8594 compliant header generation for deprecated API versions.
*
* @see ADR-008 for API versioning strategy
* @see docs/architecture/api-versioning-infrastructure.md
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Request, Response } from 'express';
import {
addDeprecationHeaders,
addDeprecationHeadersFromRequest,
DEPRECATION_HEADERS,
} from './deprecation.middleware';
import { VERSION_CONFIGS } from '../config/apiVersions';
import { createMockRequest } from '../tests/utils/createMockRequest';
// Mock the logger to avoid actual logging during tests
vi.mock('../services/logger.server', () => ({
createScopedLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
logger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
child: vi.fn().mockReturnThis(),
},
}));
describe('deprecation.middleware', () => {
// Store original VERSION_CONFIGS to restore after tests
let originalV1Config: typeof VERSION_CONFIGS.v1;
let originalV2Config: typeof VERSION_CONFIGS.v2;
let mockRequest: Request;
let mockResponse: Partial<Response>;
let mockNext: any;
let setHeaderSpy: any;
beforeEach(() => {
// Save original configs
originalV1Config = { ...VERSION_CONFIGS.v1 };
originalV2Config = { ...VERSION_CONFIGS.v2 };
// Reset mocks
setHeaderSpy = vi.fn();
mockRequest = createMockRequest({
method: 'GET',
path: '/api/v1/flyers',
get: vi.fn().mockReturnValue('TestUserAgent/1.0'),
});
mockResponse = {
set: setHeaderSpy,
setHeader: setHeaderSpy,
};
mockNext = vi.fn();
});
afterEach(() => {
// Restore original configs after each test
VERSION_CONFIGS.v1 = originalV1Config;
VERSION_CONFIGS.v2 = originalV2Config;
});
describe('addDeprecationHeaders (factory function)', () => {
describe('with active version', () => {
it('should always set X-API-Version header', () => {
// Arrange - v1 is active by default
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v1');
expect(mockNext).toHaveBeenCalledTimes(1);
});
it('should not add Deprecation header for active version', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalledWith(DEPRECATION_HEADERS.DEPRECATION, 'true');
expect(setHeaderSpy).toHaveBeenCalledTimes(1); // Only X-API-Version
});
it('should not add Sunset header for active version', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalledWith(
DEPRECATION_HEADERS.SUNSET,
expect.anything(),
);
});
it('should not add Link header for active version', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalledWith(DEPRECATION_HEADERS.LINK, expect.anything());
});
it('should not set versionDeprecation on request for active version', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(mockRequest.versionDeprecation).toBeUndefined();
});
});
describe('with deprecated version', () => {
beforeEach(() => {
// Mark v1 as deprecated for these tests
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'deprecated',
sunsetDate: '2027-01-01T00:00:00Z',
successorVersion: 'v2',
};
});
it('should add Deprecation: true header', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.DEPRECATION, 'true');
});
it('should add Sunset header with ISO 8601 date', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(
DEPRECATION_HEADERS.SUNSET,
'2027-01-01T00:00:00Z',
);
});
it('should add Link header with successor-version relation', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(
DEPRECATION_HEADERS.LINK,
'</api/v2>; rel="successor-version"',
);
});
it('should add X-API-Deprecation-Notice header', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(
DEPRECATION_HEADERS.DEPRECATION_NOTICE,
expect.stringContaining('deprecated'),
);
});
it('should always set X-API-Version header', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v1');
});
it('should set versionDeprecation on request', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(mockRequest.versionDeprecation).toBeDefined();
expect(mockRequest.versionDeprecation?.deprecated).toBe(true);
expect(mockRequest.versionDeprecation?.sunsetDate).toBe('2027-01-01T00:00:00Z');
expect(mockRequest.versionDeprecation?.successorVersion).toBe('v2');
});
it('should call next() to continue middleware chain', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledTimes(1);
expect(mockNext).toHaveBeenCalledWith();
});
it('should add all RFC 8594 compliant headers in correct format', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert - verify all headers are set
const headerCalls = setHeaderSpy.mock.calls;
const headerNames = headerCalls.map((call: unknown[]) => call[0]);
expect(headerNames).toContain(DEPRECATION_HEADERS.API_VERSION);
expect(headerNames).toContain(DEPRECATION_HEADERS.DEPRECATION);
expect(headerNames).toContain(DEPRECATION_HEADERS.SUNSET);
expect(headerNames).toContain(DEPRECATION_HEADERS.LINK);
expect(headerNames).toContain(DEPRECATION_HEADERS.DEPRECATION_NOTICE);
});
});
describe('with deprecated version missing optional fields', () => {
beforeEach(() => {
// Mark v1 as deprecated without sunset date or successor
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'deprecated',
// No sunsetDate or successorVersion
};
});
it('should add Deprecation header even without sunset date', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.DEPRECATION, 'true');
});
it('should not add Sunset header when sunsetDate is not configured', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalledWith(
DEPRECATION_HEADERS.SUNSET,
expect.anything(),
);
});
it('should not add Link header when successorVersion is not configured', () => {
// Arrange
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalledWith(DEPRECATION_HEADERS.LINK, expect.anything());
});
});
describe('with v2 version', () => {
it('should set X-API-Version: v2 header', () => {
// Arrange
const middleware = addDeprecationHeaders('v2');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v2');
});
});
});
describe('addDeprecationHeadersFromRequest', () => {
describe('when apiVersion is set on request', () => {
it('should add headers based on request apiVersion', () => {
// Arrange
mockRequest.apiVersion = 'v1';
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'deprecated',
sunsetDate: '2027-06-01T00:00:00Z',
successorVersion: 'v2',
};
// Act
addDeprecationHeadersFromRequest(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v1');
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.DEPRECATION, 'true');
expect(setHeaderSpy).toHaveBeenCalledWith(
DEPRECATION_HEADERS.SUNSET,
'2027-06-01T00:00:00Z',
);
});
it('should not add deprecation headers for active version', () => {
// Arrange
mockRequest.apiVersion = 'v2';
// Act
addDeprecationHeadersFromRequest(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v2');
expect(setHeaderSpy).toHaveBeenCalledTimes(1);
});
});
describe('when apiVersion is not set on request', () => {
it('should skip header processing and call next', () => {
// Arrange
mockRequest.apiVersion = undefined;
// Act
addDeprecationHeadersFromRequest(mockRequest, mockResponse as Response, mockNext);
// Assert
expect(setHeaderSpy).not.toHaveBeenCalled();
expect(mockNext).toHaveBeenCalledTimes(1);
});
});
});
describe('DEPRECATION_HEADERS constants', () => {
it('should have correct header names', () => {
expect(DEPRECATION_HEADERS.DEPRECATION).toBe('Deprecation');
expect(DEPRECATION_HEADERS.SUNSET).toBe('Sunset');
expect(DEPRECATION_HEADERS.LINK).toBe('Link');
expect(DEPRECATION_HEADERS.DEPRECATION_NOTICE).toBe('X-API-Deprecation-Notice');
expect(DEPRECATION_HEADERS.API_VERSION).toBe('X-API-Version');
});
});
describe('edge cases', () => {
it('should handle sunset version status', () => {
// Arrange
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'sunset',
sunsetDate: '2026-01-01T00:00:00Z',
};
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert - sunset is different from deprecated, so no deprecation headers
// Only X-API-Version should be set
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v1');
expect(setHeaderSpy).not.toHaveBeenCalledWith(DEPRECATION_HEADERS.DEPRECATION, 'true');
});
it('should handle request with existing log object', () => {
// Arrange
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'deprecated',
sunsetDate: '2027-01-01T00:00:00Z',
successorVersion: 'v2',
};
const mockLogWithBindings = {
debug: vi.fn(),
bindings: vi.fn().mockReturnValue({ request_id: 'test-request-id' }),
};
mockRequest.log = mockLogWithBindings as unknown as Request['log'];
const middleware = addDeprecationHeaders('v1');
// Act
middleware(mockRequest, mockResponse as Response, mockNext);
// Assert - should not throw and should complete
expect(mockNext).toHaveBeenCalledTimes(1);
});
it('should work with different versions in sequence', () => {
// Arrange
VERSION_CONFIGS.v1 = {
version: 'v1',
status: 'deprecated',
sunsetDate: '2027-01-01T00:00:00Z',
successorVersion: 'v2',
};
const v1Middleware = addDeprecationHeaders('v1');
const v2Middleware = addDeprecationHeaders('v2');
// Act
v1Middleware(mockRequest, mockResponse as Response, mockNext);
// Reset for v2
setHeaderSpy.mockClear();
mockNext.mockClear();
const mockRequest2 = createMockRequest({
method: 'GET',
path: '/api/v2/flyers',
});
v2Middleware(mockRequest2, mockResponse as Response, mockNext);
// Assert - v2 should only have API version header
expect(setHeaderSpy).toHaveBeenCalledWith(DEPRECATION_HEADERS.API_VERSION, 'v2');
expect(setHeaderSpy).toHaveBeenCalledTimes(1);
});
});
});