@@ -9,13 +9,15 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
99import { checkPostMethod } from "./middleware" ;
1010// We'll test the wrapped middleware as it would be used in production
1111import middleware from "./middleware" ;
12+ import { config } from "./middleware" ;
1213
1314// Mock dependencies at module level
1415vi . mock ( "@vercel/edge-config" , ( ) => ( {
1516 get : vi . fn ( ) ,
1617} ) ) ;
1718
1819vi . mock ( "next-collect/server" , ( ) => ( {
20+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1921 collectEvents : vi . fn ( ( config : any ) => config . middleware ) ,
2022} ) ) ;
2123
@@ -29,11 +31,13 @@ vi.mock("next/server", async () => {
2931 const actual = await vi . importActual < typeof import ( "next/server" ) > ( "next/server" ) ;
3032
3133 // Create a NextResponse constructor that returns Response objects
34+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3235 const NextResponse = function ( body : any , init ?: ResponseInit ) {
3336 return new Response ( body , init ) ;
3437 } ;
3538
3639 // Add static methods
40+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3741 NextResponse . json = ( body : any , init ?: ResponseInit ) => {
3842 return new Response ( JSON . stringify ( body ) , {
3943 ...init ,
@@ -55,6 +59,7 @@ vi.mock("next/server", async () => {
5559 } ) ;
5660
5761 // Add cookies property
62+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5863 ( response as any ) . cookies = {
5964 delete : vi . fn ( ) ,
6065 set : vi . fn ( ) ,
@@ -91,6 +96,7 @@ vi.mock("next/server", async () => {
9196 } ) ;
9297
9398 // Add cookies property
99+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94100 ( response as any ) . cookies = {
95101 delete : vi . fn ( ) ,
96102 set : vi . fn ( ) ,
@@ -128,6 +134,7 @@ const createTestRequest = (overrides?: {
128134 return req ;
129135} ;
130136
137+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131138const createEdgeConfigMock = ( config : Record < string , any > ) => {
132139 return ( key : string ) => {
133140 if ( key in config ) return Promise . resolve ( config [ key ] ) ;
@@ -147,6 +154,7 @@ const expectStatus = (res: Response, status: number) => {
147154
148155// Wrapper for middleware calls to handle type casting
149156const callMiddleware = async ( req : NextRequest ) : Promise < Response > => {
157+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150158 return ( await ( middleware as any ) ( req ) ) as Response ;
151159} ;
152160
@@ -165,15 +173,6 @@ describe("Middleware - POST requests restriction", () => {
165173 expect ( res1 ) . toBeNull ( ) ;
166174 } ) ;
167175
168- it ( "should block POST requests to not-allowed app routes" , async ( ) => {
169- const req = createRequest ( "/team/xyz" , "POST" ) ;
170- const res = checkPostMethod ( req ) ;
171- expect ( res ) . not . toBeNull ( ) ;
172- expect ( res ?. status ) . toBe ( 405 ) ;
173- expect ( res ?. statusText ) . toBe ( "Method Not Allowed" ) ;
174- expect ( res ?. headers . get ( "Allow" ) ) . toBe ( "GET" ) ;
175- } ) ;
176-
177176 it ( "should allow GET requests to app routes" , async ( ) => {
178177 const req = createRequest ( "/team/xyz" , "GET" ) ;
179178 const res = checkPostMethod ( req ) ;
@@ -213,29 +212,6 @@ describe("Middleware Integration Tests", () => {
213212 } ) ;
214213 } ) ;
215214
216- describe ( "POST Method Protection" , ( ) => {
217- it ( "should allow POST to /api/auth/signup" , async ( ) => {
218- const req = createTestRequest ( {
219- url : `${ WEBAPP_URL } /api/auth/signup` ,
220- method : "POST" ,
221- } ) ;
222-
223- const res = await callMiddleware ( req ) ;
224- expectStatus ( res , 200 ) ;
225- } ) ;
226-
227- it ( "should block POST to regular routes" , async ( ) => {
228- const req = createTestRequest ( {
229- url : `${ WEBAPP_URL } /team/test` ,
230- method : "POST" ,
231- } ) ;
232-
233- const res = await callMiddleware ( req ) ;
234- expectStatus ( res , 405 ) ;
235- expect ( getHeader ( res , "Allow" ) ) . toBe ( "GET" ) ;
236- } ) ;
237- } ) ;
238-
239215 describe ( "Maintenance Mode" , ( ) => {
240216 it ( "should redirect to maintenance when enabled" , async ( ) => {
241217 ( edgeConfigGet as Mock ) . mockImplementation (
@@ -430,22 +406,6 @@ describe("Middleware Integration Tests", () => {
430406 } ) ;
431407
432408 describe ( "Multiple Features" , ( ) => {
433- it ( "should handle POST protection before maintenance mode" , async ( ) => {
434- ( edgeConfigGet as Mock ) . mockImplementation (
435- createEdgeConfigMock ( {
436- isInMaintenanceMode : true ,
437- } )
438- ) ;
439-
440- const req = createTestRequest ( {
441- url : `${ WEBAPP_URL } /team/test` ,
442- method : "POST" ,
443- } ) ;
444-
445- const res = await callMiddleware ( req ) ;
446- // POST protection should trigger first
447- expectStatus ( res , 405 ) ;
448- } ) ;
449409
450410 it ( "should handle embed route with routing forms rewrite" , async ( ) => {
451411 const req = createTestRequest ( {
@@ -483,3 +443,67 @@ describe("Middleware Integration Tests", () => {
483443 } ) ;
484444 } ) ;
485445} ) ;
446+
447+ describe ( "Middleware Matcher - Comprehensive Coverage" , ( ) => {
448+ const matcher = config . matcher [ 0 ] ;
449+ const pattern = matcher . replace ( / ^ \/ | \/ $ / g, "" ) ;
450+ const regex = new RegExp ( `^/${ pattern } ` ) ;
451+
452+ const cases = [
453+ // pages & apis
454+ { path : "/" , expected : true , reason : "Root page" } ,
455+ { path : "/home" , expected : true , reason : "Regular page" } ,
456+ { path : "/team/abc" , expected : true , reason : "Nested page" } ,
457+ { path : "/api/auth/login" , expected : true , reason : "API route" } ,
458+ { path : "/api/bookings" , expected : true , reason : "Top-level API" } ,
459+ { path : "/dashboard/settings" , expected : true , reason : "Deep nested page" } ,
460+ { path : "/user/john/profile" , expected : true , reason : "Multiple nested path" } ,
461+ { path : "/apps/routing_forms/form" , expected : true , reason : "App page under /apps" } ,
462+ { path : "/embed?ui.color-scheme=dark" , expected : true , reason : "Embed query param" } ,
463+
464+ // should be ignored (internal / static / public)
465+ { path : "/_next/static/chunks/app.js" , expected : false , reason : "Internal static asset" } ,
466+ { path : "/_next/image?url=%2Flogo.png&w=256&q=75" , expected : false , reason : "Internal image handler" } ,
467+ { path : "/_next/data/build-id/page.json" , expected : false , reason : "Next.js data route" } ,
468+ { path : "/favicon.ico" , expected : false , reason : "Favicon asset" } ,
469+ { path : "/robots.txt" , expected : false , reason : "Robots file" } ,
470+ { path : "/sitemap.xml" , expected : false , reason : "Sitemap file" } ,
471+ { path : "/public/images/logo.png" , expected : false , reason : "Public folder asset" } ,
472+ { path : "/public/fonts/inter.woff2" , expected : false , reason : "Public folder font" } ,
473+ { path : "/static/js/main.js" , expected : false , reason : "Static folder JavaScript" } ,
474+ { path : "/static/css/app.css" , expected : false , reason : "Static folder stylesheet" } ,
475+
476+ // edge cases
477+ { path : "/manifest.json" , expected : true , reason : "Manifest is a public page, not ignored" } ,
478+ { path : "/_nextsomething" , expected : true , reason : "Looks like _next but not reserved" } ,
479+ { path : "/nextconfig" , expected : true , reason : "Normal route with 'next' in name" } ,
480+ { path : "/_NEXT/image" , expected : true , reason : "Case-sensitive test (should match)" } ,
481+ { path : "/favicon-abc.ico" , expected : true , reason : "Favicon variant should still match" } ,
482+ { path : "/robots-custom.txt" , expected : true , reason : "Custom robots file should match" } ,
483+ { path : "/sitemap-other.xml" , expected : true , reason : "Custom sitemap file should match" } ,
484+ { path : "/api_" , expected : true , reason : "Partial match with api underscore" } ,
485+ { path : "//double-slash" , expected : true , reason : "Double slash URL" } ,
486+ { path : "/_next" , expected : false , reason : "Bare _next path" } ,
487+ ] ;
488+
489+ it ( "should match only the intended routes" , ( ) => {
490+ for ( const { path, expected, reason } of cases ) {
491+ const result = regex . test ( path ) ;
492+ expect ( result , `${ path } → ${ reason } ` ) . toBe ( expected ) ;
493+ }
494+ } ) ;
495+
496+ it ( "should not accidentally match internal Next.js routes" , ( ) => {
497+ const internalPaths = [ "/_next/static" , "/_next/image" , "/_next/data" ] ;
498+ for ( const path of internalPaths ) {
499+ expect ( regex . test ( path ) ) . toBe ( false ) ;
500+ }
501+ } ) ;
502+
503+ it ( "should match all user-facing routes and APIs" , ( ) => {
504+ const publicPaths = [ "/" , "/api/user" , "/settings" , "/dashboard" ] ;
505+ for ( const path of publicPaths ) {
506+ expect ( regex . test ( path ) ) . toBe ( true ) ;
507+ }
508+ } ) ;
509+ } ) ;
0 commit comments