diff --git a/middleware/tests/unit/compression.middleware.spec.ts b/middleware/tests/unit/compression.middleware.spec.ts new file mode 100644 index 0000000..ae2f716 --- /dev/null +++ b/middleware/tests/unit/compression.middleware.spec.ts @@ -0,0 +1,146 @@ +import { Request, Response, NextFunction } from 'express'; +import * as zlib from 'zlib'; +import { CompressionMiddleware } from '../../src/compression/compression.middleware'; +import { COMPRESSION_CONFIG } from '../../src/compression/compression.config'; + +function makeMiddleware(): CompressionMiddleware { + return new CompressionMiddleware(); +} + +/** + * Builds a mock req/res pair and runs the middleware, then + * simulates a response body being written via res.end(). + * + * Returns a promise that resolves with the buffer passed to the + * *real* end() call so tests can inspect compressed/uncompressed output. + */ +function runMiddleware( + body: Buffer | string, + acceptEncoding: string, + contentType = 'application/json', +): Promise<{ buffer: Buffer; headers: Record }> { + return new Promise((resolve, reject) => { + const mw = makeMiddleware(); + const headers: Record = { 'Content-Type': contentType }; + + const req: Partial = { + method: 'GET', + path: '/test', + headers: { 'accept-encoding': acceptEncoding }, + } as any; + + let capturedBuffer: Buffer | null = null; + + const res: Partial & { [key: string]: any } = { + getHeader: (name: string) => headers[name], + setHeader: (name: string, value: any) => { headers[name] = value; }, + removeHeader: jest.fn(), + write: jest.fn(), + end: (chunk?: any) => { + // This is the *original* end — capture what was passed + capturedBuffer = chunk ? (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) : Buffer.alloc(0); + resolve({ buffer: capturedBuffer, headers }); + }, + } as any; + + const next: NextFunction = jest.fn(() => { + // After next() the middleware has patched res.end — call it with the body + try { + (res as any).end(Buffer.isBuffer(body) ? body : Buffer.from(body)); + } catch (err) { + reject(err); + } + }); + + mw.use(req as Request, res as Response, next); + }); +} + +describe('CompressionMiddleware', () => { + describe('passthrough — body below threshold', () => { + it('does not compress when body is smaller than threshold', async () => { + const smallBody = 'hi'; // < 1024 bytes + const { buffer, headers } = await runMiddleware(smallBody, 'gzip'); + expect(headers['Content-Encoding']).toBeUndefined(); + expect(buffer.toString()).toBe(smallBody); + }); + }); + + describe('passthrough — skipped content types', () => { + it('does not compress image/* responses even when body is large', async () => { + const largeBody = Buffer.alloc(COMPRESSION_CONFIG.threshold + 1, 'x'); + const { headers } = await runMiddleware(largeBody, 'gzip', 'image/png'); + expect(headers['Content-Encoding']).toBeUndefined(); + }); + + it('does not compress video/* responses', async () => { + const largeBody = Buffer.alloc(COMPRESSION_CONFIG.threshold + 1, 'x'); + const { headers } = await runMiddleware(largeBody, 'gzip', 'video/mp4'); + expect(headers['Content-Encoding']).toBeUndefined(); + }); + + it('does not compress audio/* responses', async () => { + const largeBody = Buffer.alloc(COMPRESSION_CONFIG.threshold + 1, 'x'); + const { headers } = await runMiddleware(largeBody, 'gzip', 'audio/mpeg'); + expect(headers['Content-Encoding']).toBeUndefined(); + }); + + it('does not compress application/zip responses', async () => { + const largeBody = Buffer.alloc(COMPRESSION_CONFIG.threshold + 1, 'x'); + const { headers } = await runMiddleware(largeBody, 'gzip', 'application/zip'); + expect(headers['Content-Encoding']).toBeUndefined(); + }); + }); + + describe('no accept-encoding header', () => { + it('does not compress when client sends no accept-encoding', async () => { + const largeBody = Buffer.alloc(COMPRESSION_CONFIG.threshold + 1, 'x').toString(); + const { headers } = await runMiddleware(largeBody, '', 'application/json'); + expect(headers['Content-Encoding']).toBeUndefined(); + }); + }); + + describe('gzip compression', () => { + it('sets Content-Encoding: gzip and returns decompressible data', async () => { + const largeBody = 'a'.repeat(COMPRESSION_CONFIG.threshold + 1); + const { buffer, headers } = await runMiddleware(largeBody, 'gzip', 'application/json'); + expect(headers['Content-Encoding']).toBe('gzip'); + const decompressed = zlib.gunzipSync(buffer).toString(); + expect(decompressed).toBe(largeBody); + }); + }); + + describe('brotli compression', () => { + it('sets Content-Encoding: br and returns decompressible data', async () => { + const largeBody = 'b'.repeat(COMPRESSION_CONFIG.threshold + 1); + const { buffer, headers } = await runMiddleware(largeBody, 'br', 'application/json'); + expect(headers['Content-Encoding']).toBe('br'); + const decompressed = zlib.brotliDecompressSync(buffer).toString(); + expect(decompressed).toBe(largeBody); + }); + }); + + describe('deflate compression', () => { + it('sets Content-Encoding: deflate and returns decompressible data', async () => { + const largeBody = 'c'.repeat(COMPRESSION_CONFIG.threshold + 1); + const { buffer, headers } = await runMiddleware(largeBody, 'deflate', 'application/json'); + expect(headers['Content-Encoding']).toBe('deflate'); + const decompressed = zlib.inflateSync(buffer).toString(); + expect(decompressed).toBe(largeBody); + }); + }); + + describe('algorithm preference (brotli > gzip > deflate)', () => { + it('prefers brotli when both br and gzip are advertised', async () => { + const largeBody = 'd'.repeat(COMPRESSION_CONFIG.threshold + 1); + const { headers } = await runMiddleware(largeBody, 'gzip, br', 'application/json'); + expect(headers['Content-Encoding']).toBe('br'); + }); + + it('uses gzip when br is not listed', async () => { + const largeBody = 'e'.repeat(COMPRESSION_CONFIG.threshold + 1); + const { headers } = await runMiddleware(largeBody, 'gzip, deflate', 'application/json'); + expect(headers['Content-Encoding']).toBe('gzip'); + }); + }); +}); diff --git a/middleware/tests/unit/correlation-id.middleware.spec.ts b/middleware/tests/unit/correlation-id.middleware.spec.ts new file mode 100644 index 0000000..20130f7 --- /dev/null +++ b/middleware/tests/unit/correlation-id.middleware.spec.ts @@ -0,0 +1,125 @@ +import { Request, Response, NextFunction } from 'express'; +import { CorrelationIdMiddleware } from '../../src/monitoring/correlation-id.middleware'; +import { CorrelationIdStorage } from '../../src/monitoring/correlation-id.storage'; + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +function makeMiddleware(): CorrelationIdMiddleware { + return new CorrelationIdMiddleware(); +} + +function mockRes() { + const headers: Record = {}; + return { + headers, + setHeader: jest.fn((name: string, value: string) => { headers[name] = value; }), + getHeader: jest.fn((name: string) => headers[name]), + } as unknown as Response; +} + +function mockReq(correlationId?: string, user?: { id: string }): Partial { + const headers: Record = {}; + if (correlationId) headers['x-correlation-id'] = correlationId; + return { method: 'GET', path: '/test', headers, user, header: (name: string) => headers[name.toLowerCase()] } as any; +} + +describe('CorrelationIdMiddleware', () => { + let next: jest.Mock; + + beforeEach(() => { + next = jest.fn(); + }); + + it('calls next()', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + mw.use(req as Request, res, next); + expect(next).toHaveBeenCalledTimes(1); + }); + + it('generates a UUID v4 when no X-Correlation-ID header is provided', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + mw.use(req as Request, res, next); + const id = (req as any).correlationId as string; + expect(id).toMatch(UUID_REGEX); + }); + + it('reuses an existing X-Correlation-ID from the request header', () => { + const existingId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'; + const mw = makeMiddleware(); + const req = mockReq(existingId); + const res = mockRes(); + mw.use(req as Request, res, next); + expect((req as any).correlationId).toBe(existingId); + }); + + it('sets X-Correlation-ID on the response', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + mw.use(req as Request, res, next); + expect((res as any).headers['X-Correlation-ID']).toBeDefined(); + expect((res as any).headers['X-Correlation-ID']).toMatch(UUID_REGEX); + }); + + it('response X-Correlation-ID matches request correlationId', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + mw.use(req as Request, res, next); + expect((res as any).headers['X-Correlation-ID']).toBe((req as any).correlationId); + }); + + it('attaches correlationId to request headers for downstream propagation', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + mw.use(req as Request, res, next); + expect(req.headers!['x-correlation-id']).toBe((req as any).correlationId); + }); + + it('makes correlationId accessible via CorrelationIdStorage inside next()', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + let storedId: string | undefined; + + next.mockImplementation(() => { + storedId = CorrelationIdStorage.getCorrelationId(); + }); + + mw.use(req as Request, res, next); + expect(storedId).toBe((req as any).correlationId); + }); + + it('stores userId from req.user.id in CorrelationIdStorage', () => { + const mw = makeMiddleware(); + const req = mockReq(undefined, { id: 'user-42' }); + const res = mockRes(); + let storedUserId: string | undefined; + + next.mockImplementation(() => { + storedUserId = CorrelationIdStorage.getUserId(); + }); + + mw.use(req as Request, res, next); + expect(storedUserId).toBe('user-42'); + }); + + it('stores undefined userId when req.user is absent', () => { + const mw = makeMiddleware(); + const req = mockReq(); + const res = mockRes(); + let storedUserId: string | undefined = 'sentinel'; + + next.mockImplementation(() => { + storedUserId = CorrelationIdStorage.getUserId(); + }); + + mw.use(req as Request, res, next); + expect(storedUserId).toBeUndefined(); + }); +}); diff --git a/middleware/tests/unit/jwt-auth.middleware.spec.ts b/middleware/tests/unit/jwt-auth.middleware.spec.ts new file mode 100644 index 0000000..fe2ec6d --- /dev/null +++ b/middleware/tests/unit/jwt-auth.middleware.spec.ts @@ -0,0 +1,209 @@ +import { Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { UnauthorizedException } from '@nestjs/common'; +import { + JwtAuthMiddleware, + JwtAuthMiddlewareOptions, + RedisClient, +} from '../../src/auth/jwt-auth.middleware'; + +const TEST_SECRET = 'test-secret-key'; + +function makeMiddleware(opts: Partial = {}): JwtAuthMiddleware { + const options: JwtAuthMiddlewareOptions = { + secret: TEST_SECRET, + logging: false, + ...opts, + }; + return new JwtAuthMiddleware(options as any); +} + +function signToken(payload: object, secret = TEST_SECRET, options?: jwt.SignOptions): string { + return jwt.sign(payload, secret, options); +} + +function mockReq(headers: Record = {}, path = '/protected'): Partial { + return { method: 'GET', path, headers } as any; +} + +function mockRes(): Partial { + return {} as Partial; +} + +describe('JwtAuthMiddleware', () => { + let next: jest.Mock; + + beforeEach(() => { + next = jest.fn(); + }); + + describe('public routes', () => { + it('calls next() without checking token for a public route', async () => { + const mw = makeMiddleware({ publicRoutes: ['/public'] }); + const req = mockReq({}, '/public/resource'); + await mw.use(req as Request, mockRes() as Response, next); + expect(next).toHaveBeenCalledTimes(1); + }); + + it('does not call next() for non-matching public route prefix', async () => { + const mw = makeMiddleware({ publicRoutes: ['/public'] }); + const req = mockReq({}, '/protected'); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('missing / malformed Authorization header', () => { + it('throws UnauthorizedException when no authorization header present', async () => { + const mw = makeMiddleware(); + const req = mockReq({}); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('throws UnauthorizedException when header does not start with Bearer', async () => { + const mw = makeMiddleware(); + const req = mockReq({ authorization: 'Basic sometoken' }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('throws UnauthorizedException when Bearer token value is empty', async () => { + const mw = makeMiddleware(); + const req = mockReq({ authorization: 'Bearer ' }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('valid token', () => { + it('calls next() and attaches user payload for a valid token', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com', userRole: 'USER' }); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect(next).toHaveBeenCalledTimes(1); + expect((req as any).user).toMatchObject({ userId: 'u1', email: 'a@b.com', userRole: 'USER' }); + }); + + it('normalizes sub claim to userId', async () => { + const token = signToken({ sub: 'u2', email: 'b@b.com' }); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect((req as any).user.userId).toBe('u2'); + }); + + it('normalizes role claim to userRole', async () => { + const token = signToken({ userId: 'u3', email: 'c@b.com', role: 'ADMIN' }); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect((req as any).user.userRole).toBe('ADMIN'); + }); + }); + + describe('invalid / expired token', () => { + it('throws UnauthorizedException for an expired token', async () => { + const token = signToken( + { userId: 'u1', email: 'a@b.com' }, + TEST_SECRET, + { expiresIn: -1 }, + ); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('throws UnauthorizedException for a token signed with wrong secret', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }, 'wrong-secret'); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('throws UnauthorizedException when payload missing email', async () => { + const token = signToken({ userId: 'u1' }); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('throws UnauthorizedException when payload missing userId/sub/id', async () => { + const token = signToken({ email: 'a@b.com' }); + const mw = makeMiddleware(); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('token blacklisting via Redis', () => { + it('throws UnauthorizedException when token is blacklisted', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }); + const redisClient: RedisClient = { + get: jest.fn().mockResolvedValue('1'), + }; + const mw = makeMiddleware({ redisClient }); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + expect(redisClient.get).toHaveBeenCalledWith(`blacklist:${token}`); + }); + + it('proceeds normally when token is not blacklisted', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }); + const redisClient: RedisClient = { + get: jest.fn().mockResolvedValue(null), + }; + const mw = makeMiddleware({ redisClient }); + const req = mockReq({ authorization: `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect(next).toHaveBeenCalledTimes(1); + }); + }); + + describe('validateUser callback', () => { + it('throws UnauthorizedException when validateUser returns falsy', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }); + const validateUser = jest.fn().mockResolvedValue(null); + const mw = makeMiddleware({ validateUser }); + const req = mockReq({ authorization: `Bearer ${token}` }); + await expect(mw.use(req as Request, mockRes() as Response, next)).rejects.toThrow( + UnauthorizedException, + ); + expect(validateUser).toHaveBeenCalledWith('u1'); + }); + + it('calls next() when validateUser returns a user object', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }); + const validateUser = jest.fn().mockResolvedValue({ id: 'u1' }); + const mw = makeMiddleware({ validateUser }); + const req = mockReq({ authorization: `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect(next).toHaveBeenCalledTimes(1); + }); + }); + + describe('custom authHeader', () => { + it('reads the token from a custom header name', async () => { + const token = signToken({ userId: 'u1', email: 'a@b.com' }); + const mw = makeMiddleware({ authHeader: 'x-custom-auth' }); + const req = mockReq({ 'x-custom-auth': `Bearer ${token}` }); + await mw.use(req as Request, mockRes() as Response, next); + expect(next).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/middleware/tests/unit/security-headers.middleware.spec.ts b/middleware/tests/unit/security-headers.middleware.spec.ts new file mode 100644 index 0000000..8b24978 --- /dev/null +++ b/middleware/tests/unit/security-headers.middleware.spec.ts @@ -0,0 +1,188 @@ +import { Request, Response, NextFunction } from 'express'; +import { SecurityHeadersMiddleware } from '../../src/security/security-headers.middleware'; +import { SECURITY_HEADERS_CONFIG } from '../../src/security/security-headers.config'; + +function makeMiddleware(): SecurityHeadersMiddleware { + return new SecurityHeadersMiddleware(); +} + +function mockReq(path = '/test'): Partial { + return { method: 'GET', path } as any; +} + +function mockRes(): { + headers: Record; + removedHeaders: string[]; + setHeader: jest.Mock; + removeHeader: jest.Mock; + getHeader: jest.Mock; + on: jest.Mock; + finishCallback?: () => void; +} { + const headers: Record = {}; + const removedHeaders: string[] = []; + + const res = { + headers, + removedHeaders, + setHeader: jest.fn((name: string, value: string) => { + headers[name] = value; + }), + removeHeader: jest.fn((name: string) => { + removedHeaders.push(name); + delete headers[name]; + }), + getHeader: jest.fn((name: string) => headers[name]), + on: jest.fn((event: string, cb: () => void) => { + if (event === 'finish') { + res.finishCallback = cb; + } + }), + finishCallback: undefined as (() => void) | undefined, + }; + return res; +} + +describe('SecurityHeadersMiddleware', () => { + let next: jest.Mock; + + beforeEach(() => { + next = jest.fn(); + delete process.env.NODE_ENV; + }); + + afterEach(() => { + delete process.env.NODE_ENV; + }); + + describe('common security headers', () => { + it('sets X-Content-Type-Options to nosniff', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.headers['X-Content-Type-Options']).toBe('nosniff'); + }); + + it('sets X-Frame-Options to DENY', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.headers['X-Frame-Options']).toBe('DENY'); + }); + + it('sets X-XSS-Protection', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.headers['X-XSS-Protection']).toBe('1; mode=block'); + }); + + it('sets all common headers defined in config', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + for (const [header, value] of Object.entries(SECURITY_HEADERS_CONFIG.common)) { + expect(res.headers[header]).toBe(value); + } + }); + + it('calls next() after setting headers', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(next).toHaveBeenCalledTimes(1); + }); + }); + + describe('removes sensitive headers', () => { + it('removes X-Powered-By', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.removedHeaders).toContain('X-Powered-By'); + }); + + it('removes Server header', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.removedHeaders).toContain('Server'); + }); + + it('removes all headers listed in config.removeHeaders', () => { + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + for (const header of SECURITY_HEADERS_CONFIG.removeHeaders) { + expect(res.removedHeaders).toContain(header); + } + }); + }); + + describe('HSTS header', () => { + it('does NOT set HSTS in non-production', () => { + process.env.NODE_ENV = 'development'; + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.headers['Strict-Transport-Security']).toBeUndefined(); + }); + + it('sets HSTS in production', () => { + process.env.NODE_ENV = 'production'; + const mw = makeMiddleware(); + const res = mockRes(); + mw.use(mockReq() as Request, res as unknown as Response, next); + expect(res.headers['Strict-Transport-Security']).toBe( + SECURITY_HEADERS_CONFIG.hsts.production, + ); + }); + }); + + describe('Cache-Control on finish', () => { + it('sets no-cache for application/json response', () => { + const mw = makeMiddleware(); + const res = mockRes(); + res.headers['Content-Type'] = 'application/json'; + mw.use(mockReq() as Request, res as unknown as Response, next); + res.finishCallback?.(); + expect(res.headers['Cache-Control']).toBe(SECURITY_HEADERS_CONFIG.cacheControl.dynamic); + }); + + it('sets public max-age for text/html response', () => { + const mw = makeMiddleware(); + const res = mockRes(); + res.headers['Content-Type'] = 'text/html'; + mw.use(mockReq() as Request, res as unknown as Response, next); + res.finishCallback?.(); + expect(res.headers['Cache-Control']).toBe(SECURITY_HEADERS_CONFIG.cacheControl.static); + }); + + it('sets public max-age for text/css response', () => { + const mw = makeMiddleware(); + const res = mockRes(); + res.headers['Content-Type'] = 'text/css'; + mw.use(mockReq() as Request, res as unknown as Response, next); + res.finishCallback?.(); + expect(res.headers['Cache-Control']).toBe(SECURITY_HEADERS_CONFIG.cacheControl.static); + }); + + it('sets private no-cache for unknown content type', () => { + const mw = makeMiddleware(); + const res = mockRes(); + res.headers['Content-Type'] = 'application/octet-stream'; + mw.use(mockReq() as Request, res as unknown as Response, next); + res.finishCallback?.(); + expect(res.headers['Cache-Control']).toBe(SECURITY_HEADERS_CONFIG.cacheControl.private); + }); + + it('does not set Cache-Control when Content-Type is absent', () => { + const mw = makeMiddleware(); + const res = mockRes(); + // no Content-Type set + mw.use(mockReq() as Request, res as unknown as Response, next); + res.finishCallback?.(); + expect(res.headers['Cache-Control']).toBeUndefined(); + }); + }); +});