@@ -4,6 +4,8 @@ import { createSchema } from 'graphql-yoga'
44import getPort from 'get-port'
55import { WobeGraphqlYogaPlugin } from '.'
66
7+ const sleep = ( ms : number ) => new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
8+
79describe ( 'Wobe GraphQL Yoga plugin' , ( ) => {
810 it ( 'should reject GET requests by default' , async ( ) => {
911 const port = await getPort ( )
@@ -164,6 +166,341 @@ describe('Wobe GraphQL Yoga plugin', () => {
164166 wobe . stop ( )
165167 } )
166168
169+ it ( 'should block queries that exceed max depth' , async ( ) => {
170+ const port = await getPort ( )
171+ const wobe = new Wobe ( )
172+
173+ await wobe . usePlugin (
174+ WobeGraphqlYogaPlugin ( {
175+ maxDepth : 2 ,
176+ typeDefs : `
177+ type Query {
178+ hello: Hello
179+ }
180+
181+ type Hello {
182+ nested: Nested
183+ }
184+
185+ type Nested {
186+ value: String
187+ }
188+ ` ,
189+ resolvers : {
190+ Query : {
191+ hello : ( ) => ( { nested : { value : 'ok' } } ) ,
192+ } ,
193+ } ,
194+ } ) ,
195+ )
196+
197+ wobe . listen ( port )
198+
199+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
200+ method : 'POST' ,
201+ headers : { 'Content-Type' : 'application/json' } ,
202+ body : JSON . stringify ( {
203+ query : `
204+ query TooDeep {
205+ hello { nested { value } }
206+ }
207+ ` ,
208+ } ) ,
209+ } )
210+
211+ const body = await res . json ( )
212+ expect ( body . data ) . toBeUndefined ( )
213+ expect ( body . errors ?. [ 0 ] ?. message ) . toContain ( 'max depth' )
214+
215+ wobe . stop ( )
216+ } )
217+
218+ it ( 'should block queries that exceed max cost' , async ( ) => {
219+ const port = await getPort ( )
220+ const wobe = new Wobe ( )
221+
222+ await wobe . usePlugin (
223+ WobeGraphqlYogaPlugin ( {
224+ maxCost : 2 ,
225+ typeDefs : `
226+ type Query {
227+ a: String
228+ b: String
229+ c: String
230+ }
231+ ` ,
232+ resolvers : {
233+ Query : {
234+ a : ( ) => 'a' ,
235+ b : ( ) => 'b' ,
236+ c : ( ) => 'c' ,
237+ } ,
238+ } ,
239+ } ) ,
240+ )
241+
242+ wobe . listen ( port )
243+
244+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
245+ method : 'POST' ,
246+ headers : { 'Content-Type' : 'application/json' } ,
247+ body : JSON . stringify ( {
248+ query : `
249+ query TooExpensive {
250+ a
251+ b
252+ c
253+ }
254+ ` ,
255+ } ) ,
256+ } )
257+
258+ const body = await res . json ( )
259+ expect ( body . data ) . toBeUndefined ( )
260+ expect ( body . errors ?. [ 0 ] ?. message ) . toContain ( 'too expensive' )
261+
262+ wobe . stop ( )
263+ } )
264+
265+ it ( 'should reject multiple operations by default' , async ( ) => {
266+ const port = await getPort ( )
267+ const wobe = new Wobe ( )
268+
269+ await wobe . usePlugin (
270+ WobeGraphqlYogaPlugin ( {
271+ typeDefs : `
272+ type Query {
273+ a: String
274+ b: String
275+ }
276+ ` ,
277+ resolvers : {
278+ Query : {
279+ a : ( ) => 'a' ,
280+ b : ( ) => 'b' ,
281+ } ,
282+ } ,
283+ } ) ,
284+ )
285+
286+ wobe . listen ( port )
287+
288+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
289+ method : 'POST' ,
290+ headers : { 'Content-Type' : 'application/json' } ,
291+ body : JSON . stringify ( {
292+ query : `
293+ query One { a }
294+ query Two { b }
295+ ` ,
296+ } ) ,
297+ } )
298+
299+ const body = await res . json ( )
300+ expect ( body . data ) . toBeUndefined ( )
301+ expect ( body . errors ?. [ 0 ] ?. message ) . toMatch (
302+ / M u l t i p l e o p e r a t i o n s | C o u l d n o t d e t e r m i n e / i,
303+ )
304+
305+ wobe . stop ( )
306+ } )
307+
308+ it ( 'should allow only whitelisted operation names when provided' , async ( ) => {
309+ const port = await getPort ( )
310+ const wobe = new Wobe ( )
311+
312+ await wobe . usePlugin (
313+ WobeGraphqlYogaPlugin ( {
314+ allowedOperationNames : [ 'AllowedOp' ] ,
315+ allowMultipleOperations : false ,
316+ typeDefs : `
317+ type Query {
318+ a: String
319+ }
320+ ` ,
321+ resolvers : {
322+ Query : {
323+ a : ( ) => 'a' ,
324+ } ,
325+ } ,
326+ } ) ,
327+ )
328+
329+ wobe . listen ( port )
330+
331+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
332+ method : 'POST' ,
333+ headers : { 'Content-Type' : 'application/json' } ,
334+ body : JSON . stringify ( {
335+ query : `
336+ query NotAllowed { a }
337+ ` ,
338+ } ) ,
339+ } )
340+
341+ const body = await res . json ( )
342+ expect ( body . data ) . toBeUndefined ( )
343+ expect ( body . errors ?. [ 0 ] ?. message ) . toContain ( 'not allowed' )
344+
345+ wobe . stop ( )
346+ } )
347+
348+ it ( 'should reject requests that exceed maxRequestSizeBytes' , async ( ) => {
349+ const port = await getPort ( )
350+ const wobe = new Wobe ( )
351+
352+ await wobe . usePlugin (
353+ WobeGraphqlYogaPlugin ( {
354+ maxRequestSizeBytes : 10 ,
355+ typeDefs : `
356+ type Query {
357+ a: String
358+ }
359+ ` ,
360+ resolvers : {
361+ Query : {
362+ a : ( ) => 'a' ,
363+ } ,
364+ } ,
365+ } ) ,
366+ )
367+
368+ wobe . listen ( port )
369+
370+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
371+ method : 'POST' ,
372+ headers : { 'Content-Type' : 'application/json' } ,
373+ body : JSON . stringify ( {
374+ query : `
375+ query LargePayload { a }
376+ ` ,
377+ } ) ,
378+ } )
379+
380+ expect ( res . status ) . toBe ( 413 )
381+ wobe . stop ( )
382+ } )
383+
384+ it ( 'should timeout when resolver exceeds timeoutMs' , async ( ) => {
385+ const port = await getPort ( )
386+ const wobe = new Wobe ( )
387+
388+ await wobe . usePlugin (
389+ WobeGraphqlYogaPlugin ( {
390+ timeoutMs : 10 ,
391+ typeDefs : `
392+ type Query {
393+ slow: String
394+ }
395+ ` ,
396+ resolvers : {
397+ Query : {
398+ slow : async ( ) => {
399+ await sleep ( 50 )
400+ return 'slow'
401+ } ,
402+ } ,
403+ } ,
404+ } ) ,
405+ )
406+
407+ wobe . listen ( port )
408+
409+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
410+ method : 'POST' ,
411+ headers : { 'Content-Type' : 'application/json' } ,
412+ body : JSON . stringify ( {
413+ query : `
414+ query Slow { slow }
415+ ` ,
416+ } ) ,
417+ } )
418+
419+ expect ( res . status ) . toBe ( 504 )
420+ wobe . stop ( )
421+ } )
422+
423+ it ( 'should allow rateLimiter to block requests' , async ( ) => {
424+ const port = await getPort ( )
425+ const wobe = new Wobe ( )
426+
427+ await wobe . usePlugin (
428+ WobeGraphqlYogaPlugin ( {
429+ rateLimiter : async ( ) =>
430+ new Response ( 'Too Many Requests' , { status : 429 } ) ,
431+ typeDefs : `
432+ type Query {
433+ hello: String
434+ }
435+ ` ,
436+ resolvers : {
437+ Query : {
438+ hello : ( ) => 'Hello' ,
439+ } ,
440+ } ,
441+ } ) ,
442+ )
443+
444+ wobe . listen ( port )
445+
446+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
447+ method : 'POST' ,
448+ headers : { 'Content-Type' : 'application/json' } ,
449+ body : JSON . stringify ( {
450+ query : `
451+ query Test { hello }
452+ ` ,
453+ } ) ,
454+ } )
455+
456+ expect ( res . status ) . toBe ( 429 )
457+ wobe . stop ( )
458+ } )
459+
460+ it ( 'should call onRequestResolved hook with timing info' , async ( ) => {
461+ const port = await getPort ( )
462+ const wobe = new Wobe ( )
463+ let called = false
464+ let status : number | undefined
465+
466+ await wobe . usePlugin (
467+ WobeGraphqlYogaPlugin ( {
468+ onRequestResolved : ( input ) => {
469+ called = true
470+ status = input . status
471+ } ,
472+ typeDefs : `
473+ type Query {
474+ hello: String
475+ }
476+ ` ,
477+ resolvers : {
478+ Query : {
479+ hello : ( ) => 'Hello' ,
480+ } ,
481+ } ,
482+ } ) ,
483+ )
484+
485+ wobe . listen ( port )
486+
487+ const res = await fetch ( `http://127.0.0.1:${ port } /graphql` , {
488+ method : 'POST' ,
489+ headers : { 'Content-Type' : 'application/json' } ,
490+ body : JSON . stringify ( {
491+ query : `
492+ query Hook { hello }
493+ ` ,
494+ } ) ,
495+ } )
496+
497+ expect ( res . status ) . toBe ( 200 )
498+ expect ( called ) . toBe ( true )
499+ expect ( status ) . toBe ( 200 )
500+
501+ wobe . stop ( )
502+ } )
503+
167504 it ( 'should set the wobe response in the graphql context with record' , async ( ) => {
168505 const port = await getPort ( )
169506 const wobe = new Wobe < { customType : string } > ( ) . beforeHandler ( ( ctx ) => {
0 commit comments