From fd9ec72d102feb77b7ec9dca908b79517b6c1dff Mon Sep 17 00:00:00 2001 From: "Abdulmalik A." Date: Thu, 26 Mar 2026 20:12:27 +0100 Subject: [PATCH] Revert "Conditional middleware execution" --- backend/package.json | 2 - backend/src/common/middleware/utils/README.md | 150 -------- ...conditional.middleware.integration.spec.ts | 304 ---------------- .../utils/conditional.middleware.spec.ts | 330 ------------------ .../utils/conditional.middleware.ts | 87 ----- backend/src/index.ts | 3 - ...conditional.middleware.integration.spec.ts | 238 ------------- package-lock.json | 23 +- 8 files changed, 6 insertions(+), 1131 deletions(-) delete mode 100644 backend/src/common/middleware/utils/README.md delete mode 100644 backend/src/common/middleware/utils/conditional.middleware.integration.spec.ts delete mode 100644 backend/src/common/middleware/utils/conditional.middleware.spec.ts delete mode 100644 backend/src/common/middleware/utils/conditional.middleware.ts delete mode 100644 backend/src/index.ts delete mode 100644 backend/test/conditional.middleware.integration.spec.ts diff --git a/backend/package.json b/backend/package.json index e20023b..daa2f36 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,7 +33,6 @@ "@nestjs/swagger": "^11.2.5", "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", - "@types/micromatch": "^4.0.10", "@types/passport-google-oauth20": "^2.0.16", "@types/pdfkit": "^0.14.0", "bcryptjs": "^3.0.2", @@ -44,7 +43,6 @@ "google-auth-library": "^9.15.1", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.2", - "micromatch": "^4.0.8", "nodemailer": "^7.0.12", "oauth2client": "^1.0.0", "passport": "^0.7.0", diff --git a/backend/src/common/middleware/utils/README.md b/backend/src/common/middleware/utils/README.md deleted file mode 100644 index 0571a3d..0000000 --- a/backend/src/common/middleware/utils/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# Conditional Middleware Utilities - -This module provides higher-order middleware wrappers that allow you to conditionally apply middleware based on route patterns. - -## Installation - -The utilities are exported from `src/index.ts`: - -```typescript -import { unless, onlyFor, RoutePattern } from '@/index'; -``` - -## Usage - -### `unless(middleware, excludePatterns)` - -Skips middleware execution for routes matching the provided patterns. - -```typescript -import { unless } from '@/index'; -import { CorrelationIdMiddleware } from './correlation-id.middleware'; - -// Skip correlation ID for health and metrics endpoints -const conditionalMiddleware = unless( - new CorrelationIdMiddleware(), - ['/health', '/metrics', '/api/*/health'] -); - -// Apply in your module -app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); -``` - -### `onlyFor(middleware, includePatterns)` - -Executes middleware only for routes matching the provided patterns. - -```typescript -import { onlyFor } from '@/index'; -import { AuthMiddleware } from './auth.middleware'; - -// Apply auth middleware only to admin routes -const conditionalMiddleware = onlyFor( - new AuthMiddleware(), - ['/api/admin/*', '/admin/**'] -); - -// Apply in your module -app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); -``` - -## Pattern Types - -The utilities support three types of patterns: - -### 1. Exact Strings -```typescript -unless(middleware, '/health') -``` - -### 2. Regular Expressions -```typescript -unless(middleware, /^\/api\/v\d+\/status$/) -``` - -### 3. Glob Patterns -```typescript -unless(middleware, [ - '/api/*/metrics', - '/static/**', - '/admin/**/users/**' -]) -``` - -## Examples - -### Skip middleware for static assets -```typescript -const conditionalMiddleware = unless( - new LoggingMiddleware(), - [ - '/static/**', - '/assets/**', - '/**/*.css', - '/**/*.js', - '/**/*.png', - '/**/*.jpg' - ] -); -``` - -### Apply middleware only to API routes -```typescript -const conditionalMiddleware = onlyFor( - new RateLimitMiddleware(), - [ - '/api/**', - '!/api/docs/**' // Exclude API docs - ] -); -``` - -### Complex routing scenarios -```typescript -// Skip authentication for public routes -const publicRoutes = [ - '/health', - '/metrics', - '/auth/login', - '/auth/register', - '/public/**', - '/api/v1/public/**' -]; - -const conditionalAuth = unless( - new AuthMiddleware(), - publicRoutes -); -``` - -## Performance - -The conditional middleware is designed to have minimal overhead: - -- Zero overhead for non-matching routes (early return) -- Efficient pattern matching using micromatch -- Stateless implementation -- No memory leaks - -## Error Handling - -The utilities gracefully handle: - -- Invalid patterns (treated as non-matching) -- Null/undefined patterns (treated as non-matching) -- Malformed regex patterns (fallback to string comparison) -- Empty pattern arrays (treated as non-matching) - -## TypeScript Support - -Full TypeScript support with proper type definitions: - -```typescript -import { RoutePattern } from '@/index'; - -const patterns: RoutePattern = [ - '/api/users', // string - /^\/api\/v\d+/, // regex - '/admin/**' // glob -]; -``` diff --git a/backend/src/common/middleware/utils/conditional.middleware.integration.spec.ts b/backend/src/common/middleware/utils/conditional.middleware.integration.spec.ts deleted file mode 100644 index 9322635..0000000 --- a/backend/src/common/middleware/utils/conditional.middleware.integration.spec.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { unless, onlyFor } from './conditional.middleware'; - -// Simple test middleware for integration testing -@Injectable() -class TestLoggingMiddleware implements NestMiddleware { - public static calls: Array<{ path: string; timestamp: number }> = []; - - use(req: Request, res: Response, next: NextFunction): void { - TestLoggingMiddleware.calls.push({ - path: req.path || req.url || '/', - timestamp: Date.now(), - }); - next(); - } - - static reset(): void { - TestLoggingMiddleware.calls = []; - } -} - -// Second test middleware for chaining tests -@Injectable() -class TestAuthMiddleware implements NestMiddleware { - public static calls: Array<{ path: string; timestamp: number }> = []; - - use(req: Request, res: Response, next: NextFunction): void { - TestAuthMiddleware.calls.push({ - path: req.path || req.url || '/', - timestamp: Date.now(), - }); - next(); - } - - static reset(): void { - TestAuthMiddleware.calls = []; - } -} - -describe('Conditional Middleware Integration Tests', () => { - let middleware: TestLoggingMiddleware; - let mockReq: any; - let mockRes: Partial; - let mockNext: NextFunction; - - beforeEach(() => { - TestLoggingMiddleware.reset(); - middleware = new TestLoggingMiddleware(); - - mockRes = { - setHeader: jest.fn(), - getHeader: jest.fn(), - }; - mockNext = jest.fn(); - }); - - describe('Real-world usage scenarios', () => { - it('should work with typical API route patterns', () => { - // Skip logging for health and metrics endpoints - const conditionalMiddleware = unless(middleware, [ - '/health', - '/metrics', - '/api/*/health', - '/api/*/metrics', - ]); - - // Test various routes - const routes = [ - { path: '/health', shouldLog: false }, - { path: '/metrics', shouldLog: false }, - { path: '/api/v1/health', shouldLog: false }, - { path: '/api/v2/metrics', shouldLog: false }, - { path: '/api/v1/users', shouldLog: true }, - { path: '/api/v2/posts', shouldLog: true }, - { path: '/auth/login', shouldLog: true }, - ]; - - routes.forEach((route) => { - mockReq = { path: route.path, url: route.path }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - }); - - const loggedPaths = TestLoggingMiddleware.calls.map((call) => call.path); - const expectedLoggedPaths = routes - .filter((route) => route.shouldLog) - .map((route) => route.path); - - expect(loggedPaths).toEqual(expectedLoggedPaths); - expect(loggedPaths).not.toContain('/health'); - expect(loggedPaths).not.toContain('/metrics'); - }); - - it('should handle admin-only middleware with onlyFor', () => { - // Only apply logging middleware to admin routes - const conditionalMiddleware = onlyFor(middleware, [ - '/api/admin/*', - '/api/v*/admin/**', - /^\/admin\//, - ]); - - const routes = [ - { path: '/api/admin/users', shouldLog: true }, - { path: '/api/v1/admin/settings', shouldLog: true }, - { path: '/admin/dashboard', shouldLog: true }, - { path: '/api/v1/users', shouldLog: false }, - { path: '/api/v2/posts', shouldLog: false }, - { path: '/public/home', shouldLog: false }, - ]; - - routes.forEach((route) => { - mockReq = { path: route.path, url: route.path }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - }); - - const loggedPaths = TestLoggingMiddleware.calls.map((call) => call.path); - const expectedLoggedPaths = routes - .filter((route) => route.shouldLog) - .map((route) => route.path); - - expect(loggedPaths).toEqual(expectedLoggedPaths); - expect(loggedPaths).toContain('/api/admin/users'); - expect(loggedPaths).not.toContain('/api/v1/users'); - }); - - it('should handle complex routing scenarios', () => { - // Skip middleware for static assets and API documentation - const conditionalMiddleware = unless(middleware, [ - '/static/**', - '/assets/**', - '/docs/**', - '/api/docs/**', - '/swagger/**', - /\.(css|js|ico|png|jpg|jpeg|gif|svg)$/, // Static file extensions - ]); - - const routes = [ - { path: '/static/css/main.css', shouldLog: false }, - { path: '/assets/images/logo.png', shouldLog: false }, - { path: '/docs/api.html', shouldLog: false }, - { path: '/api/docs/v1', shouldLog: false }, - { path: '/swagger/ui', shouldLog: false }, - { path: '/favicon.ico', shouldLog: false }, - { path: '/api/v1/users', shouldLog: true }, - { path: '/auth/login', shouldLog: true }, - { path: '/dashboard', shouldLog: true }, - ]; - - routes.forEach((route) => { - mockReq = { path: route.path, url: route.path }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - }); - - const loggedPaths = TestLoggingMiddleware.calls.map((call) => call.path); - const expectedLoggedPaths = routes - .filter((route) => route.shouldLog) - .map((route) => route.path); - - expect(loggedPaths).toEqual(expectedLoggedPaths); - expect(loggedPaths).not.toContain('/static/css/main.css'); - expect(loggedPaths).not.toContain('/favicon.ico'); - expect(loggedPaths).toContain('/api/v1/users'); - }); - }); - - describe('Edge cases and error handling', () => { - it('should handle empty pattern arrays', () => { - const conditionalMiddleware = unless(middleware, [] as any); - - mockReq = { path: '/test', url: '/test' }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - - expect(TestLoggingMiddleware.calls).toHaveLength(1); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should handle null/undefined patterns gracefully', () => { - expect(() => { - const conditionalMiddleware1 = unless(middleware, null as any); - const conditionalMiddleware2 = onlyFor(middleware, undefined as any); - - mockReq = { path: '/test', url: '/test' }; - conditionalMiddleware1.use(mockReq, mockRes as Response, mockNext); - conditionalMiddleware2.use(mockReq, mockRes as Response, mockNext); - }).not.toThrow(); - }); - - it('should handle malformed regex patterns', () => { - expect(() => { - const conditionalMiddleware = unless(middleware, [ - /invalid regex/ as any, - ]); - mockReq = { path: '/test', url: '/test' }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - }).not.toThrow(); - }); - - it('should handle very long paths', () => { - const longPath = '/api/v1/' + 'segment/'.repeat(100) + 'endpoint'; - const conditionalMiddleware = unless(middleware, ['/api/v1/**'] as any); - - mockReq = { path: longPath, url: longPath }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - - expect(TestLoggingMiddleware.calls).toHaveLength(0); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('Performance under load', () => { - it('should handle large numbers of route patterns efficiently', () => { - // Create a large array of patterns - const patterns: any[] = []; - for (let i = 0; i < 1000; i++) { - patterns.push(`/api/v${i}/**`); - } - patterns.push('/health', '/metrics'); - - const conditionalMiddleware = unless(middleware, patterns); - - const start = Date.now(); - - // Test with many routes - for (let i = 0; i < 1000; i++) { - mockReq = { path: `/api/v500/test${i}`, url: `/api/v500/test${i}` }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - TestLoggingMiddleware.reset(); - } - - const end = Date.now(); - const duration = end - start; - - // Should complete quickly even with many patterns - expect(duration).toBeLessThan(10000); - }); - - it('should maintain performance with nested glob patterns', () => { - const complexPatterns: any[] = [ - '/api/**/users/**', - '/api/**/posts/**', - '/api/**/comments/**', - '/admin/**/settings/**', - '/admin/**/users/**', - ]; - - const conditionalMiddleware = onlyFor(middleware, complexPatterns); - - const start = Date.now(); - - const testRoutes = [ - '/api/v1/users/123/posts', - '/api/v2/posts/456/comments', - '/admin/panel/settings/general', - '/api/v1/users/789/profile', - '/admin/dashboard/users/list', - ]; - - testRoutes.forEach((route) => { - mockReq = { path: route, url: route }; - conditionalMiddleware.use(mockReq, mockRes as Response, mockNext); - TestLoggingMiddleware.reset(); - }); - - const end = Date.now(); - const duration = end - start; - - // Should handle complex patterns efficiently - expect(duration).toBeLessThan(50); - }); - }); - - describe('Middleware chaining', () => { - it('should work correctly when multiple conditional middlewares are chained', () => { - // First middleware: skip for health routes - const firstConditional = unless(middleware, ['/health']); - - // Second middleware: only for admin routes - const secondMiddleware = new TestAuthMiddleware(); - const secondConditional = onlyFor(secondMiddleware, ['/admin/**']); - - const routes = [ - { path: '/health', firstShouldLog: false, secondShouldLog: false }, - { path: '/admin/users', firstShouldLog: true, secondShouldLog: true }, - { path: '/api/users', firstShouldLog: true, secondShouldLog: false }, - ]; - - routes.forEach((route) => { - TestLoggingMiddleware.reset(); - TestAuthMiddleware.reset(); - - mockReq = { path: route.path, url: route.path }; - - firstConditional.use(mockReq, mockRes as Response, mockNext); - const firstLogged = TestLoggingMiddleware.calls.length > 0; - - secondConditional.use(mockReq, mockRes as Response, mockNext); - const secondLogged = TestAuthMiddleware.calls.length > 0; - - expect(firstLogged).toBe(route.firstShouldLog); - expect(secondLogged).toBe(route.secondShouldLog); - }); - }); - }); -}); diff --git a/backend/src/common/middleware/utils/conditional.middleware.spec.ts b/backend/src/common/middleware/utils/conditional.middleware.spec.ts deleted file mode 100644 index b742613..0000000 --- a/backend/src/common/middleware/utils/conditional.middleware.spec.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { unless, onlyFor, RoutePattern } from './conditional.middleware'; - -// Mock middleware for testing -@Injectable() -class TestMiddleware implements NestMiddleware { - public static callCount = 0; - public static lastPath: string = ''; - - use(req: Request, res: Response, next: NextFunction): void { - TestMiddleware.callCount++; - TestMiddleware.lastPath = req.path || req.url || '/'; - next(); - } - - static reset(): void { - TestMiddleware.callCount = 0; - TestMiddleware.lastPath = ''; - } -} - -describe('Conditional Middleware', () => { - let middleware: TestMiddleware; - let mockReq: any; - let mockRes: Partial; - let mockNext: NextFunction; - - beforeEach(() => { - TestMiddleware.reset(); - middleware = new TestMiddleware(); - - mockReq = { - path: '/test', - url: '/test', - }; - - mockRes = {}; - mockNext = jest.fn(); - }); - - describe('matchesPath', () => { - const testMatchesPath = ( - path: string, - pattern: RoutePattern, - expected: boolean, - ) => { - // Import the private function through reflection for testing - const conditionalMiddleware = require('./conditional.middleware'); - - // We'll test the public behavior through unless/onlyFor instead - }; - - describe('exact string matching', () => { - it('should match exact strings', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - mockReq.path = '/health'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should not match different strings', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - mockReq.path = '/api/users'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(1); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('regex pattern matching', () => { - it('should match regex patterns', async () => { - const wrappedMiddleware = unless(middleware, /^\/health/); - - mockReq.path = '/health/detailed'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should not match non-matching regex', async () => { - const wrappedMiddleware = unless(middleware, /^\/health/); - - mockReq.path = '/api/users'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(1); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('glob pattern matching', () => { - it('should match glob patterns', async () => { - const wrappedMiddleware = unless(middleware, '/api/*/users'); - - mockReq.path = '/api/v1/users'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should match complex glob patterns', async () => { - const wrappedMiddleware = unless(middleware, '/api/**/metrics'); - - mockReq.path = '/api/v1/system/metrics'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should not match non-matching glob patterns', async () => { - const wrappedMiddleware = unless(middleware, '/api/*/users'); - - mockReq.path = '/api/v1/posts'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(1); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('array of patterns', () => { - it('should match any pattern in array', async () => { - const wrappedMiddleware = unless(middleware, [ - '/health', - '/metrics', - /^\/api\/v\d+\/status/, - ]); - - // Test first pattern - mockReq.path = '/health'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - expect(TestMiddleware.callCount).toBe(0); - - TestMiddleware.reset(); - - // Test second pattern - mockReq.path = '/metrics'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - expect(TestMiddleware.callCount).toBe(0); - - TestMiddleware.reset(); - - // Test regex pattern - mockReq.path = '/api/v2/status'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - expect(TestMiddleware.callCount).toBe(0); - }); - - it('should not match when no patterns match', async () => { - const wrappedMiddleware = unless(middleware, ['/health', '/metrics']); - - mockReq.path = '/api/users'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - - expect(TestMiddleware.callCount).toBe(1); - expect(mockNext).toHaveBeenCalled(); - }); - }); - }); - - describe('unless', () => { - it('should skip middleware for excluded routes', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - mockReq.path = '/health'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should execute middleware for non-excluded routes', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - mockReq.path = '/api/users'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(1); - expect(TestMiddleware.lastPath).toBe('/api/users'); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should handle req.url fallback when req.path is undefined', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - delete mockReq.path; - mockReq.url = '/health'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should handle fallback to root path when both are undefined', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - delete mockReq.path; - delete mockReq.url; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(1); - expect(TestMiddleware.lastPath).toBe('/'); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('onlyFor', () => { - it('should execute middleware only for specified routes', async () => { - const wrappedMiddleware = onlyFor(middleware, '/api/v1/users'); - - mockReq.path = '/api/v1/users'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(1); - expect(TestMiddleware.lastPath).toBe('/api/v1/users'); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should skip middleware for non-specified routes', async () => { - const wrappedMiddleware = onlyFor(middleware, '/api/v1/users'); - - mockReq.path = '/api/v1/posts'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(0); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should work with regex patterns', async () => { - const wrappedMiddleware = onlyFor(middleware, /^\/api\/v\d+\/users/); - - mockReq.path = '/api/v2/users'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(1); - expect(TestMiddleware.lastPath).toBe('/api/v2/users'); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should work with glob patterns', async () => { - const wrappedMiddleware = onlyFor(middleware, '/api/*/users'); - - mockReq.path = '/api/v1/users'; - wrappedMiddleware.use(mockReq as Request, mockRes as Response, mockNext); - - expect(TestMiddleware.callCount).toBe(1); - expect(TestMiddleware.lastPath).toBe('/api/v1/users'); - expect(mockNext).toHaveBeenCalled(); - }); - }); - - describe('performance', () => { - it('should have minimal overhead for non-matching routes', async () => { - const wrappedMiddleware = unless(middleware, '/health'); - - const start = process.hrtime.bigint(); - - // Run many iterations to measure overhead - for (let i = 0; i < 10000; i++) { - mockReq.path = '/api/users'; - wrappedMiddleware.use( - mockReq as Request, - mockRes as Response, - mockNext, - ); - TestMiddleware.reset(); - } - - const end = process.hrtime.bigint(); - const duration = Number(end - start) / 1000000; // Convert to milliseconds - - // Should complete very quickly (less than 100ms for 10k iterations) - expect(duration).toBeLessThan(100); - }); - }); -}); diff --git a/backend/src/common/middleware/utils/conditional.middleware.ts b/backend/src/common/middleware/utils/conditional.middleware.ts deleted file mode 100644 index 9016c0d..0000000 --- a/backend/src/common/middleware/utils/conditional.middleware.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import * as micromatch from 'micromatch'; - -export type RoutePattern = string | RegExp | (string | RegExp)[]; - -/** - * Checks if the request path matches any of the provided patternsss - */ -function matchesPath(path: string, patterns: RoutePattern): boolean { - if (!patterns || (Array.isArray(patterns) && patterns.length === 0)) { - return false; - } - - if (Array.isArray(patterns)) { - return patterns.some((pattern) => matchesPath(path, pattern)); - } - - if (patterns instanceof RegExp) { - return patterns.test(path); - } - - // Handle empty strings and invalid patterns - if (typeof patterns !== 'string' || patterns.trim() === '') { - return false; - } - - // Handle glob patterns and exact strings with micromatch - try { - return micromatch.isMatch(path, patterns); - } catch (error) { - // If micromatch fails, fall back to exact string comparison - return path === patterns; - } -} - -/** - * Higher-order middleware wrapper that skips execution for specified routes - * - * @param middleware - The NestJS middleware to wrap - * @param excludePatterns - Route patterns to exclude (string, regex, or glob) - * @returns Wrapped middleware that skips execution for matching routes - */ -export function unless( - middleware: T, - excludePatterns: RoutePattern, -): T { - return new (class { - use(req: Request, res: Response, next: NextFunction): void { - const requestPath = req.path || req.url || '/'; - - // If path matches exclude patterns, skip middleware - if (matchesPath(requestPath, excludePatterns)) { - return next(); - } - - // Otherwise, execute the original middleware - return middleware.use(req, res, next); - } - })() as T; -} - -/** - * Higher-order middleware wrapper that executes only for specified routes - * - * @param middleware - The NestJS middleware to wrap - * @param includePatterns - Route patterns to include (string, regex, or glob) - * @returns Wrapped middleware that executes only for matching routes - */ -export function onlyFor( - middleware: T, - includePatterns: RoutePattern, -): T { - return new (class { - use(req: Request, res: Response, next: NextFunction): void { - const requestPath = req.path || req.url || '/'; - - // If path doesn't match include patterns, skip middleware - if (!matchesPath(requestPath, includePatterns)) { - return next(); - } - - // Otherwise, execute the original middleware - return middleware.use(req, res, next); - } - })() as T; -} diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 69f7f59..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Export conditional middleware utilities -export { unless, onlyFor } from './common/middleware/utils/conditional.middleware'; -export type { RoutePattern } from './common/middleware/utils/conditional.middleware'; diff --git a/backend/test/conditional.middleware.integration.spec.ts b/backend/test/conditional.middleware.integration.spec.ts deleted file mode 100644 index 53db73f..0000000 --- a/backend/test/conditional.middleware.integration.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from '../src/app.module'; -import { CorrelationIdMiddleware } from '../src/common/middleware/correlation-id.middleware'; -import { - unless, - onlyFor, -} from '../src/common/middleware/utils/conditional.middleware'; - -describe('Conditional Middleware Integration Tests', () => { - let app: INestApplication; - let correlationMiddleware: CorrelationIdMiddleware; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - correlationMiddleware = new CorrelationIdMiddleware(); - - // Set up global prefix to match main.ts - app.setGlobalPrefix('api', { - exclude: ['health', 'health/*path'], - }); - - await app.init(); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('unless() integration', () => { - it('should skip correlation middleware for health endpoint', async () => { - // Apply conditional middleware to skip correlation ID for health routes - const conditionalMiddleware = unless(correlationMiddleware, [ - '/health', - '/api/health', - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - const response = await request(app.getHttpServer()) - .get('/health') - .expect(200); - - // Health endpoint should not have correlation ID header when skipped - expect(response.headers['x-correlation-id']).toBeUndefined(); - }); - - it('should apply correlation middleware for non-excluded routes', async () => { - // Apply conditional middleware to skip correlation ID for health routes only - const conditionalMiddleware = unless(correlationMiddleware, ['/health']); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - const response = await request(app.getHttpServer()) - .get('/api/v1/users') - .expect(404); // We expect 404 since the route doesn't exist, but middleware should run - - // Non-excluded endpoint should have correlation ID header - expect(response.headers['x-correlation-id']).toBeDefined(); - expect(typeof response.headers['x-correlation-id']).toBe('string'); - }); - - it('should work with glob patterns', async () => { - // Apply conditional middleware to skip correlation ID for API metrics routes - const conditionalMiddleware = unless(correlationMiddleware, [ - '/api/*/metrics', - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - const response = await request(app.getHttpServer()) - .get('/api/v1/metrics') - .expect(404); // Route doesn't exist but should be skipped - - // Metrics endpoint should not have correlation ID header when skipped - expect(response.headers['x-correlation-id']).toBeUndefined(); - }); - - it('should work with regex patterns', async () => { - // Apply conditional middleware to skip correlation ID for status routes - const conditionalMiddleware = unless(correlationMiddleware, [ - /^\/api\/v\d+\/status$/, - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - const response = await request(app.getHttpServer()) - .get('/api/v2/status') - .expect(404); // Route doesn't exist but should be skipped - - // Status endpoint should not have correlation ID header when skipped - expect(response.headers['x-correlation-id']).toBeUndefined(); - }); - }); - - describe('onlyFor() integration', () => { - it('should apply correlation middleware only for specified routes', async () => { - // Apply conditional middleware to only run correlation ID for admin routes - const conditionalMiddleware = onlyFor(correlationMiddleware, [ - '/api/admin/*', - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - // Admin route should have correlation ID - const adminResponse = await request(app.getHttpServer()) - .get('/api/admin/users') - .expect(404); // Route doesn't exist but middleware should run - - expect(adminResponse.headers['x-correlation-id']).toBeDefined(); - - // Regular route should not have correlation ID - const userResponse = await request(app.getHttpServer()) - .get('/api/v1/users') - .expect(404); // Route doesn't exist and middleware should be skipped - - expect(userResponse.headers['x-correlation-id']).toBeUndefined(); - }); - - it('should work with multiple patterns', async () => { - // Apply conditional middleware to only run for admin and billing routes - const conditionalMiddleware = onlyFor(correlationMiddleware, [ - '/api/admin/*', - '/api/billing/*', - /^\/api\/v\d+\/audit/, - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - // Test admin route - const adminResponse = await request(app.getHttpServer()) - .get('/api/admin/users') - .expect(404); - - expect(adminResponse.headers['x-correlation-id']).toBeDefined(); - - // Test billing route - const billingResponse = await request(app.getHttpServer()) - .get('/api/billing/invoices') - .expect(404); - - expect(billingResponse.headers['x-correlation-id']).toBeDefined(); - - // Test audit route with regex - const auditResponse = await request(app.getHttpServer()) - .get('/api/v2/audit/logs') - .expect(404); - - expect(auditResponse.headers['x-correlation-id']).toBeDefined(); - - // Test non-matching route - const userResponse = await request(app.getHttpServer()) - .get('/api/v1/users') - .expect(404); - - expect(userResponse.headers['x-correlation-id']).toBeUndefined(); - }); - }); - - describe('Performance Integration', () => { - it('should have minimal overhead for conditional middleware', async () => { - const conditionalMiddleware = unless(correlationMiddleware, [ - '/health', - '/metrics', - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - const start = Date.now(); - - // Make multiple requests to test performance - const promises: Promise[] = []; - for (let i = 0; i < 100; i++) { - promises.push(request(app.getHttpServer()).get('/api/test')); - } - - await Promise.all(promises); - const end = Date.now(); - - const duration = end - start; - - // Should complete quickly (less than 1 second for 100 requests) - expect(duration).toBeLessThan(1000); - }); - - it('should handle high concurrency without issues', async () => { - const conditionalMiddleware = onlyFor(correlationMiddleware, [ - '/api/secure/*', - ]); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - - // Test concurrent requests to both matching and non-matching routes - const promises: Promise[] = []; - for (let i = 0; i < 50; i++) { - promises.push(request(app.getHttpServer()).get('/api/secure/data')); - promises.push(request(app.getHttpServer()).get('/api/public/data')); - } - - const responses = await Promise.all(promises); - - // Secure routes should have correlation ID - const secureResponses = responses.slice(0, 50); - secureResponses.forEach((response: any) => { - expect(response.headers['x-correlation-id']).toBeDefined(); - }); - - // Public routes should not have correlation ID - const publicResponses = responses.slice(50); - publicResponses.forEach((response: any) => { - expect(response.headers['x-correlation-id']).toBeUndefined(); - }); - }); - }); - - describe('Error Handling', () => { - it('should handle malformed patterns gracefully', async () => { - // This should not throw an error - expect(() => { - const conditionalMiddleware = unless(correlationMiddleware, ['']); - app.use(conditionalMiddleware.use.bind(conditionalMiddleware)); - }).not.toThrow(); - }); - - it('should handle undefined request path gracefully', async () => { - const conditionalMiddleware = unless(correlationMiddleware, ['/health']); - - // Create a mock request with no path - const mockReq = { url: undefined }; - const mockRes = { setHeader: jest.fn() }; - const mockNext = jest.fn(); - - // Should not throw an error - expect(() => { - conditionalMiddleware.use(mockReq as any, mockRes as any, mockNext); - }).not.toThrow(); - - expect(mockNext).toHaveBeenCalled(); - }); - }); -}); diff --git a/package-lock.json b/package-lock.json index f785f24..693741f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "@nestjs/swagger": "^11.2.5", "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", - "@types/micromatch": "^4.0.10", "@types/passport-google-oauth20": "^2.0.16", "@types/pdfkit": "^0.14.0", "bcryptjs": "^3.0.2", @@ -56,7 +55,6 @@ "google-auth-library": "^9.15.1", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.2", - "micromatch": "^4.0.8", "nodemailer": "^7.0.12", "oauth2client": "^1.0.0", "passport": "^0.7.0", @@ -5302,12 +5300,6 @@ "@types/node": "*" } }, - "node_modules/@types/braces": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", - "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", - "license": "MIT" - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5474,15 +5466,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/micromatch": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", - "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", - "license": "MIT", - "dependencies": { - "@types/braces": "*" - } - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -7323,6 +7306,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9866,6 +9850,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11379,6 +11364,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13402,6 +13388,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14791,6 +14778,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -17349,6 +17337,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0"