Tests: include github ref in concurrency group
[jquery.git] / test / middleware-mockserver.cjs
blob0b513f04272b217b3fa9fc8d80d78acf01b9878d
1 "use strict";
3 const url = require( "node:url" );
4 const fs = require( "node:fs" );
5 const getRawBody = require( "raw-body" );
6 const multiparty = require( "multiparty" );
8 let cspLog = "";
10 /**
11  * Keep in sync with /test/mock.php
12  */
13 function cleanCallback( callback ) {
14         return callback.replace( /[^a-z0-9_]/gi, "" );
17 const mocks = {
18         contentType: function( req, resp ) {
19                 resp.writeHead( 200, {
20                         "content-type": req.query.contentType
21                 } );
22                 resp.end( req.query.response );
23         },
24         wait: function( req, resp ) {
25                 const wait = Number( req.query.wait ) * 1000;
26                 setTimeout( function() {
27                         if ( req.query.script ) {
28                                 resp.writeHead( 200, { "content-type": "text/javascript" } );
29                         } else {
30                                 resp.writeHead( 200, { "content-type": "text/html" } );
31                                 resp.end( "ERROR <script>QUnit.assert.ok( true, \"mock executed\" );</script>" );
32                         }
33                 }, wait );
34         },
35         name: function( req, resp, next ) {
36                 resp.writeHead( 200 );
37                 if ( req.query.name === "foo" ) {
38                         resp.end( "bar" );
39                         return;
40                 }
41                 getBody( req ).then( function( body ) {
42                         if ( body === "name=peter" ) {
43                                 resp.end( "pan" );
44                         } else {
45                                 resp.end( "ERROR" );
46                         }
47                 }, next );
48         },
49         xml: function( req, resp, next ) {
50                 const content = "<math><calculation>5-2</calculation><result>3</result></math>";
51                 resp.writeHead( 200, { "content-type": "text/xml" } );
53                 if ( req.query.cal === "5-2" ) {
54                         resp.end( content );
55                         return;
56                 }
57                 getBody( req ).then( function( body ) {
58                         if ( body === "cal=5-2" ) {
59                                 resp.end( content );
60                         } else {
61                                 resp.end( "<error>ERROR</error>" );
62                         }
63                 }, next );
64         },
65         atom: function( _req, resp ) {
66                 resp.writeHead( 200, { "content-type": "atom+xml" } );
67                 resp.end( "<root><element /></root>" );
68         },
69         script: function( req, resp ) {
70                 const headers = {};
71                 if ( req.query.header === "ecma" ) {
72                         headers[ "content-type" ] = "application/ecmascript";
73                 } else if ( "header" in req.query ) {
74                         headers[ "content-type" ] = "text/javascript";
75                 } else {
76                         headers[ "content-type" ] = "text/html";
77                 }
79                 if ( req.query.cors ) {
80                         headers[ "access-control-allow-origin" ] = "*";
81                 }
83                 resp.writeHead( 200, headers );
85                 if ( req.query.callback ) {
86                         resp.end( `${ cleanCallback( req.query.callback ) }(${ JSON.stringify( {
87                                 headers: req.headers
88                         } ) })` );
89                 } else {
90                         resp.end( "QUnit.assert.ok( true, \"mock executed\" );" );
91                 }
92         },
93         testbar: function( _req, resp ) {
94                 resp.writeHead( 200 );
95                 resp.end(
96                         "this.testBar = 'bar'; " +
97                         "jQuery('#ap').html('bar'); " +
98                         "QUnit.assert.ok( true, 'mock executed');"
99                 );
100         },
101         json: function( req, resp ) {
102                 const headers = {};
103                 if ( req.query.header ) {
104                         headers[ "content-type" ] = "application/json";
105                 }
106                 if ( req.query.cors ) {
107                         headers[ "access-control-allow-origin" ] = "*";
108                 }
109                 resp.writeHead( 200, headers );
110                 if ( req.query.array ) {
111                         resp.end( JSON.stringify(
112                                 [ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
113                         ) );
114                 } else {
115                         resp.end( JSON.stringify(
116                                 { data: { lang: "en", length: 25 } }
117                         ) );
118                 }
119         },
120         jsonp: function( req, resp, next ) {
121                 let callback;
122                 if ( Array.isArray( req.query.callback ) ) {
123                         callback = Promise.resolve( req.query.callback[ req.query.callback.length - 1 ] );
124                 } else if ( req.query.callback ) {
125                         callback = Promise.resolve( req.query.callback );
126                 } else if ( req.method === "GET" ) {
127                         callback = Promise.resolve( req.url.match( /^.+\/([^\/?]+)\?.+$/ )[ 1 ] );
128                 } else {
129                         callback = getBody( req ).then( function( body ) {
130                                 return body.trim().replace( "callback=", "" );
131                         } );
132                 }
133                 const json = req.query.array ?
134                         JSON.stringify(
135                                 [ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
136                         ) :
137                         JSON.stringify(
138                                 { data: { lang: "en", length: 25 } }
139                         );
140                 callback.then( function( cb ) {
141                         resp.end( `${ cleanCallback( cb ) }(${ json })` );
142                 }, next );
143         },
144         xmlOverJsonp: function( req, resp ) {
145                 const callback = req.query.callback;
146                 const body = fs.readFileSync( `${ __dirname }/data/with_fries.xml` ).toString();
147                 resp.writeHead( 200 );
148                 resp.end( `${ cleanCallback( callback ) }(${ JSON.stringify( body ) })\n` );
149         },
150         formData: function( req, resp, next ) {
151                 const prefix = "multipart/form-data; boundary=--";
152                 const contentTypeValue = req.headers[ "content-type" ];
153                 resp.writeHead( 200 );
154                 if ( ( prefix || "" ).startsWith( prefix ) ) {
155                         getMultiPartContent( req ).then( function( { fields = {} } ) {
156                                 resp.end( `key1 -> ${ fields.key1 }, key2 -> ${ fields.key2 }` );
157                         }, next );
158                 } else {
159                         resp.end( `Incorrect Content-Type: ${ contentTypeValue
160                                 }\nExpected prefix: ${ prefix }` );
161                 }
162         },
163         error: function( req, resp ) {
164                 if ( req.query.json ) {
165                         resp.writeHead( 400, { "content-type": "application/json" } );
166                         resp.end( "{ \"code\": 40, \"message\": \"Bad Request\" }" );
167                 } else {
168                         resp.writeHead( 400 );
169                         resp.end( "plain text message" );
170                 }
171         },
172         headers: function( req, resp ) {
173                 const headers = {
174                         "Sample-Header": "Hello World",
175                         "Empty-Header": "",
176                         "Sample-Header2": "Hello World 2",
177                         "List-Header": "Item 1",
178                         "list-header": "Item 2",
179                         "constructor": "prototype collision (constructor)"
180                 };
182                 // Use resp.append in express to
183                 // avoid overwriting List-Header
184                 if ( resp.append ) {
186                         for ( const key in headers ) {
187                                 resp.append( key, headers[ key ] );
188                         }
189                 } else {
190                         resp.writeHead( 200, headers );
191                 }
192                 req.query.keys.split( "|" ).forEach( function( key ) {
193                         if ( key.toLowerCase() in req.headers ) {
194                                 resp.write( `${ key }: ${ req.headers[ key.toLowerCase() ] }\n` );
195                         }
196                 } );
197                 resp.end();
198         },
199         echoData: function( req, resp, next ) {
200                 getBody( req ).then( function( body ) {
201                         resp.end( body );
202                 }, next );
203         },
204         echoQuery: function( req, resp ) {
205                 resp.end( req.parsed.search.slice( 1 ) );
206         },
207         echoMethod: function( req, resp ) {
208                 resp.end( req.method );
209         },
210         echoHtml: function( req, resp, next ) {
211                 resp.writeHead( 200, { "Content-Type": "text/html" } );
212                 resp.write( `<div id='method'>${ req.method }</div>` );
213                 resp.write( `<div id='query'>${ req.parsed.search.slice( 1 ) }</div>` );
214                 getBody( req ).then( function( body ) {
215                         resp.write( `<div id='data'>${ body }</div>` );
216                         resp.end( body );
217                 }, next );
218         },
219         etag: function( req, resp ) {
220                 const hash = Number( req.query.ts ).toString( 36 );
221                 const etag = `W/"${ hash }"`;
222                 if ( req.headers[ "if-none-match" ] === etag ) {
223                         resp.writeHead( 304 );
224                         resp.end();
225                         return;
226                 }
227                 resp.writeHead( 200, {
228                         "Etag": etag
229                 } );
230                 resp.end();
231         },
232         ims: function( req, resp ) {
233                 const ts = req.query.ts;
234                 if ( req.headers[ "if-modified-since" ] === ts ) {
235                         resp.writeHead( 304 );
236                         resp.end();
237                         return;
238                 }
239                 resp.writeHead( 200, {
240                         "Last-Modified": ts
241                 } );
242                 resp.end();
243         },
244         status: function( req, resp ) {
245                 resp.writeHead( Number( req.query.code ) );
246                 resp.end();
247         },
248         testHTML: function( req, resp ) {
249                 resp.writeHead( 200, { "Content-Type": "text/html" } );
250                 const body = fs
251                         .readFileSync( `${ __dirname }/data/test.include.html` )
252                         .toString()
253                         .replace( /{{baseURL}}/g, req.query.baseURL );
254                 resp.end( body );
255         },
256         cspFrame: function( _req, resp ) {
257                 resp.writeHead( 200, {
258                         "Content-Type": "text/html",
259                         "Content-Security-Policy": "default-src 'self'; require-trusted-types-for 'script'; " +
260                                 "report-uri /test/data/mock.php?action=cspLog"
261                 } );
262                 const body = fs.readFileSync( `${ __dirname }/data/csp.include.html` ).toString();
263                 resp.end( body );
264         },
265         cspNonce: function( req, resp ) {
266                 const testParam = req.query.test ? `-${ req.query.test }` : "";
267                 resp.writeHead( 200, {
268                         "Content-Type": "text/html",
269                         "Content-Security-Policy": "script-src 'nonce-jquery+hardcoded+nonce'; " +
270                                 "report-uri /test/data/mock.php?action=cspLog"
271                 } );
272                 const body = fs.readFileSync(
273                         `${ __dirname }/data/csp-nonce${ testParam }.html` ).toString();
274                 resp.end( body );
275         },
276         cspAjaxScript: function( _req, resp ) {
277                 resp.writeHead( 200, {
278                         "Content-Type": "text/html",
279                         "Content-Security-Policy": "script-src 'self'; " +
280                                 "report-uri /test/data/mock.php?action=cspLog"
281                 } );
282                 const body = fs.readFileSync(
283                         `${ __dirname }/data/csp-ajax-script.html` ).toString();
284                 resp.end( body );
285         },
286         cspLog: function( _req, resp ) {
287                 cspLog = "error";
288                 resp.writeHead( 200 );
289                 resp.end();
290         },
291         cspClean: function( _req, resp ) {
292                 cspLog = "";
293                 resp.writeHead( 200 );
294                 resp.end();
295         },
296         trustedHtml: function( _req, resp ) {
297                 resp.writeHead( 200, {
298                         "Content-Type": "text/html",
299                         "Content-Security-Policy": "require-trusted-types-for 'script'; " +
300                                 "report-uri /test/data/mock.php?action=cspLog"
301                 } );
302                 const body = fs.readFileSync( `${ __dirname }/data/trusted-html.html` ).toString();
303                 resp.end( body );
304         },
305         trustedTypesAttributes: function( _req, resp ) {
306                 resp.writeHead( 200, {
307                         "Content-Type": "text/html",
308                         "Content-Security-Policy": "require-trusted-types-for 'script'; " +
309                                 "report-uri /test/data/mock.php?action=cspLog"
310                 } );
311                 const body = fs.readFileSync(
312                         `${ __dirname }/data/trusted-types-attributes.html` ).toString();
313                 resp.end( body );
314         },
315         errorWithScript: function( req, resp ) {
316                 if ( req.query.withScriptContentType ) {
317                         resp.writeHead( 404, { "Content-Type": "application/javascript" } );
318                 } else {
319                         resp.writeHead( 404, { "Content-Type": "text/html; charset=UTF-8" } );
320                 }
321                 if ( req.query.callback ) {
322                         resp.end( `${ cleanCallback( req.query.callback )
323                                 }( {"status": 404, "msg": "Not Found"} )` );
324                 } else {
325                         resp.end( "QUnit.assert.ok( false, \"Mock return erroneously executed\" );" );
326                 }
327         }
329 const handlers = {
330         "test/data/mock.php": function( req, resp, next ) {
331                 if ( !mocks[ req.query.action ] ) {
332                         resp.writeHead( 400 );
333                         resp.end( "Invalid action query.\n" );
334                         console.log( "Invalid action query:", req.method, req.url );
335                         return;
336                 }
337                 mocks[ req.query.action ]( req, resp, next );
338         },
339         "test/data/support/csp.log": function( _req, resp ) {
340                 resp.writeHead( 200 );
341                 resp.end( cspLog );
342         },
343         "test/data/404.txt": function( _req, resp ) {
344                 resp.writeHead( 404 );
345                 resp.end( "" );
346         }
350  * Connect-compatible middleware factory for mocking server responses.
351  * Used by Ajax unit tests when run via Karma.
353  * Despite Karma using Express, it uses Connect to deal with custom middleware,
354  * which passes the raw Node Request and Response objects instead of the
355  * Express versions of these (e.g. no req.path, req.query, resp.set).
356  */
357 function MockserverMiddlewareFactory() {
359         /**
360          * @param {http.IncomingMessage} req
361          * @param {http.ServerResponse} resp
362          * @param {Function} next Continue request handling
363          */
364         return function( req, resp, next ) {
365                 const parsed = url.parse( req.url, /* parseQuery */ true );
366                 let path = parsed.pathname;
367                 const query = parsed.query;
368                 const subReq = Object.assign( Object.create( req ), {
369                         query: query,
370                         parsed: parsed
371                 } );
373                 if ( /^\/?test\/data\/mock.php\/?/.test( path ) ) {
375                         // Support REST-like Apache PathInfo
376                         path = "test\/data\/mock.php";
377                 }
379                 if ( !handlers[ path ] ) {
380                         next();
381                         return;
382                 }
384                 // console.log( "Mock handling", req.method, parsed.href );
385                 handlers[ path ]( subReq, resp, next );
386         };
389 function getBody( req ) {
390         return req.method !== "POST" ?
391                 Promise.resolve( "" ) :
392                 getRawBody( req, {
393                         encoding: true
394                 } );
397 function getMultiPartContent( req ) {
398         return new Promise( function( resolve ) {
399                 if ( req.method !== "POST" ) {
400                         resolve( "" );
401                         return;
402                 }
404                 const form = new multiparty.Form();
405                 form.parse( req, function( _err, fields, files ) {
406                         resolve( { fields, files } );
407                 } );
408         } );
411 module.exports = MockserverMiddlewareFactory;