1+ import type * as ClerkBackend from '@clerk/backend' ;
12import type { Request , RequestHandler , Response } from 'express' ;
23import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
34
@@ -12,7 +13,19 @@ vi.mock('@clerk/backend/proxy', async () => {
1213 } ;
1314} ) ;
1415
15- import { authenticateRequest } from '../authenticateRequest' ;
16+ const { mockCreateClerkClient } = vi . hoisted ( ( ) => ( {
17+ mockCreateClerkClient : vi . fn ( ) ,
18+ } ) ) ;
19+ vi . mock ( '@clerk/backend' , async ( ) => {
20+ const actual = await vi . importActual < typeof ClerkBackend > ( '@clerk/backend' ) ;
21+ mockCreateClerkClient . mockImplementation ( actual . createClerkClient ) ;
22+ return {
23+ ...actual ,
24+ createClerkClient : mockCreateClerkClient ,
25+ } ;
26+ } ) ;
27+
28+ import { authenticateAndDecorateRequest , authenticateRequest } from '../authenticateRequest' ;
1629import { clerkMiddleware } from '../clerkMiddleware' ;
1730import { getAuth } from '../getAuth' ;
1831import { assertNoDebugHeaders , assertSignedOutDebugHeaders , runMiddleware , runMiddlewareOnPath } from './helpers' ;
@@ -125,6 +138,176 @@ describe('clerkMiddleware', () => {
125138 ) ;
126139 } ) ;
127140
141+ it ( 'forwards arbitrary AuthenticateRequestOptions/VerifyTokenOptions to authenticateRequest' , async ( ) => {
142+ const authenticateRequestMock = vi . fn ( ) . mockResolvedValue ( { } ) ;
143+ const clerkClient = {
144+ authenticateRequest : authenticateRequestMock ,
145+ } as any ;
146+
147+ const organizationSyncOptions = {
148+ organizationPatterns : [ '/orgs/:slug' ] ,
149+ } ;
150+
151+ await authenticateRequest ( {
152+ clerkClient,
153+ request : {
154+ method : 'GET' ,
155+ url : '/' ,
156+ headers : {
157+ host : 'example.com' ,
158+ } ,
159+ } as Request ,
160+ options : {
161+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
162+ secretKey : 'sk_test_....' ,
163+ clockSkewInMs : 12_345 ,
164+ audience : 'https://api.example.com' ,
165+ authorizedParties : [ 'https://example.com' ] ,
166+ jwtKey : 'jwt-key-value' ,
167+ acceptsToken : 'session_token' ,
168+ organizationSyncOptions,
169+ skipJwksCache : true ,
170+ headerType : 'JWT' ,
171+ } as any ,
172+ } ) ;
173+
174+ expect ( authenticateRequestMock ) . toHaveBeenCalledWith (
175+ expect . any ( Object ) ,
176+ expect . objectContaining ( {
177+ audience : 'https://api.example.com' ,
178+ authorizedParties : [ 'https://example.com' ] ,
179+ clockSkewInMs : 12_345 ,
180+ jwtKey : 'jwt-key-value' ,
181+ acceptsToken : 'session_token' ,
182+ organizationSyncOptions,
183+ skipJwksCache : true ,
184+ headerType : 'JWT' ,
185+ } ) ,
186+ ) ;
187+ } ) ;
188+
189+ it ( 'does not forward middleware-only options (clerkClient, debug, frontendApiProxy) to authenticateRequest' , async ( ) => {
190+ const authenticateRequestMock = vi . fn ( ) . mockResolvedValue ( { } ) ;
191+ const clerkClient = {
192+ authenticateRequest : authenticateRequestMock ,
193+ } as any ;
194+
195+ await authenticateRequest ( {
196+ clerkClient,
197+ request : {
198+ method : 'GET' ,
199+ url : '/' ,
200+ headers : {
201+ host : 'example.com' ,
202+ } ,
203+ } as Request ,
204+ options : {
205+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
206+ secretKey : 'sk_test_....' ,
207+ clerkClient,
208+ debug : true ,
209+ frontendApiProxy : { enabled : true , path : '/__clerk' } ,
210+ } ,
211+ } ) ;
212+
213+ const forwarded = authenticateRequestMock . mock . calls [ 0 ] [ 1 ] ;
214+ expect ( forwarded ) . not . toHaveProperty ( 'clerkClient' ) ;
215+ expect ( forwarded ) . not . toHaveProperty ( 'debug' ) ;
216+ expect ( forwarded ) . not . toHaveProperty ( 'frontendApiProxy' ) ;
217+ } ) ;
218+
219+ describe ( 'apiUrl/apiVersion default-client construction' , ( ) => {
220+ beforeEach ( ( ) => {
221+ mockCreateClerkClient . mockClear ( ) ;
222+ } ) ;
223+
224+ it ( 'builds a per-middleware ClerkClient with apiUrl when no custom clerkClient is supplied' , ( ) => {
225+ authenticateAndDecorateRequest ( {
226+ apiUrl : 'https://api.example.test' ,
227+ secretKey : 'sk_test_....' ,
228+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
229+ } ) ;
230+
231+ expect ( mockCreateClerkClient ) . toHaveBeenCalledWith (
232+ expect . objectContaining ( { apiUrl : 'https://api.example.test' } ) ,
233+ ) ;
234+ } ) ;
235+
236+ it ( 'builds a per-middleware ClerkClient with apiVersion when no custom clerkClient is supplied' , ( ) => {
237+ authenticateAndDecorateRequest ( {
238+ apiVersion : 'v2' ,
239+ secretKey : 'sk_test_....' ,
240+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
241+ } ) ;
242+
243+ expect ( mockCreateClerkClient ) . toHaveBeenCalledWith ( expect . objectContaining ( { apiVersion : 'v2' } ) ) ;
244+ } ) ;
245+
246+ it ( 'does not call createClerkClient at construction when apiUrl/apiVersion are not set' , ( ) => {
247+ authenticateAndDecorateRequest ( { secretKey : 'sk_test_....' } ) ;
248+
249+ expect ( mockCreateClerkClient ) . not . toHaveBeenCalled ( ) ;
250+ } ) ;
251+
252+ it ( 'does not build a per-middleware client when the caller supplies their own clerkClient' , ( ) => {
253+ const customClient = { authenticateRequest : vi . fn ( ) } as any ;
254+
255+ authenticateAndDecorateRequest ( {
256+ apiUrl : 'https://api.example.test' ,
257+ apiVersion : 'v2' ,
258+ clerkClient : customClient ,
259+ } ) ;
260+
261+ expect ( mockCreateClerkClient ) . not . toHaveBeenCalled ( ) ;
262+ } ) ;
263+
264+ it ( 'routes outbound API traffic to the apiUrl override' , async ( ) => {
265+ const fetchSpy = vi . spyOn ( globalThis , 'fetch' ) . mockResolvedValue (
266+ new Response ( '{"data":[],"total_count":0}' , {
267+ status : 200 ,
268+ headers : { 'content-type' : 'application/json' } ,
269+ } ) ,
270+ ) ;
271+
272+ authenticateAndDecorateRequest ( {
273+ apiUrl : 'https://api.example.test' ,
274+ secretKey : 'sk_test_....' ,
275+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
276+ } ) ;
277+
278+ const client = mockCreateClerkClient . mock . results [ 0 ] . value ;
279+ await client . users . getUserList ( ) . catch ( ( ) => undefined ) ;
280+
281+ const calledUrls = fetchSpy . mock . calls . map ( call => {
282+ const input = call [ 0 ] ;
283+ if ( typeof input === 'string' ) {
284+ return input ;
285+ }
286+ if ( input instanceof URL ) {
287+ return input . href ;
288+ }
289+ return input . url ;
290+ } ) ;
291+ expect ( calledUrls . some ( url => new URL ( url ) . origin === 'https://api.example.test' ) ) . toBe ( true ) ;
292+
293+ fetchSpy . mockRestore ( ) ;
294+ } ) ;
295+
296+ it ( 'callback form: builds a per-middleware ClerkClient when the callback returns apiUrl' , async ( ) => {
297+ await runMiddleware (
298+ clerkMiddleware ( ( ) => ( {
299+ apiUrl : 'https://api.example.test' ,
300+ secretKey : 'sk_test_....' ,
301+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
302+ } ) ) ,
303+ ) . expect ( 200 ) ;
304+
305+ expect ( mockCreateClerkClient ) . toHaveBeenCalledWith (
306+ expect . objectContaining ( { apiUrl : 'https://api.example.test' } ) ,
307+ ) ;
308+ } ) ;
309+ } ) ;
310+
128311 it ( 'throws error if clerkMiddleware is not executed before getAuth' , async ( ) => {
129312 const customMiddleware : RequestHandler = ( request , response , next ) => {
130313 const auth = getAuth ( request ) ;
0 commit comments