3 const url = require( "node:url" );
4 const fs = require( "node:fs" );
5 const getRawBody = require( "raw-body" );
6 const multiparty = require( "multiparty" );
11 * Like `readFileSync`, but on error returns "ERROR"
15 function readFileSync( path ) {
17 return fs.readFileSync( path );
24 * Keep in sync with /test/mock.php
26 function cleanCallback( callback ) {
27 return callback.replace( /[^a-z0-9_]/gi, "" );
31 contentType: function( req, resp ) {
32 resp.writeHead( 200, {
33 "content-type": req.query.contentType
35 resp.end( req.query.response );
37 wait: function( req, resp ) {
38 const wait = Number( req.query.wait ) * 1000;
39 setTimeout( function() {
40 if ( req.query.script ) {
41 resp.writeHead( 200, { "content-type": "text/javascript" } );
43 resp.writeHead( 200, { "content-type": "text/html" } );
44 resp.end( "ERROR <script>QUnit.assert.ok( true, \"mock executed\" );</script>" );
48 name: function( req, resp, next ) {
49 resp.writeHead( 200 );
50 if ( req.query.name === "foo" ) {
54 getBody( req ).then( function( body ) {
55 if ( body === "name=peter" ) {
62 xml: function( req, resp, next ) {
63 const content = "<math><calculation>5-2</calculation><result>3</result></math>";
64 resp.writeHead( 200, { "content-type": "text/xml" } );
66 if ( req.query.cal === "5-2" ) {
70 getBody( req ).then( function( body ) {
71 if ( body === "cal=5-2" ) {
74 resp.end( "<error>ERROR</error>" );
78 atom: function( _req, resp ) {
79 resp.writeHead( 200, { "content-type": "atom+xml" } );
80 resp.end( "<root><element /></root>" );
82 script: function( req, resp ) {
84 if ( req.query.header === "ecma" ) {
85 headers[ "content-type" ] = "application/ecmascript";
86 } else if ( "header" in req.query ) {
87 headers[ "content-type" ] = "text/javascript";
89 headers[ "content-type" ] = "text/html";
92 if ( req.query.cors ) {
93 headers[ "access-control-allow-origin" ] = "*";
96 resp.writeHead( 200, headers );
98 if ( req.query.callback ) {
99 resp.end( `${ cleanCallback( req.query.callback ) }(${ JSON.stringify( {
103 resp.end( "QUnit.assert.ok( true, \"mock executed\" );" );
106 testbar: function( _req, resp ) {
107 resp.writeHead( 200 );
109 "this.testBar = 'bar'; " +
110 "jQuery('#ap').html('bar'); " +
111 "QUnit.assert.ok( true, 'mock executed');"
114 json: function( req, resp ) {
116 if ( req.query.header ) {
117 headers[ "content-type" ] = "application/json";
119 if ( req.query.cors ) {
120 headers[ "access-control-allow-origin" ] = "*";
122 resp.writeHead( 200, headers );
123 if ( req.query.array ) {
124 resp.end( JSON.stringify(
125 [ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
128 resp.end( JSON.stringify(
129 { data: { lang: "en", length: 25 } }
133 jsonp: function( req, resp, next ) {
135 if ( Array.isArray( req.query.callback ) ) {
136 callback = Promise.resolve( req.query.callback[ req.query.callback.length - 1 ] );
137 } else if ( req.query.callback ) {
138 callback = Promise.resolve( req.query.callback );
139 } else if ( req.method === "GET" ) {
140 callback = Promise.resolve( req.url.match( /^.+\/([^\/?]+)\?.+$/ )[ 1 ] );
142 callback = getBody( req ).then( function( body ) {
143 return body.trim().replace( "callback=", "" );
146 const json = req.query.array ?
148 [ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
151 { data: { lang: "en", length: 25 } }
153 callback.then( function( cb ) {
154 resp.end( `${ cleanCallback( cb ) }(${ json })` );
157 xmlOverJsonp: function( req, resp ) {
158 const callback = req.query.callback;
159 const body = readFileSync( `${ __dirname }/data/with_fries.xml` ).toString();
160 resp.writeHead( 200 );
161 resp.end( `${ cleanCallback( callback ) }(${ JSON.stringify( body ) })\n` );
163 formData: function( req, resp, next ) {
164 const prefix = "multipart/form-data; boundary=--";
165 const contentTypeValue = req.headers[ "content-type" ];
166 resp.writeHead( 200 );
167 if ( ( prefix || "" ).startsWith( prefix ) ) {
168 getMultiPartContent( req ).then( function( { fields = {} } ) {
169 resp.end( `key1 -> ${ fields.key1 }, key2 -> ${ fields.key2 }` );
172 resp.end( `Incorrect Content-Type: ${ contentTypeValue
173 }\nExpected prefix: ${ prefix }` );
176 error: function( req, resp ) {
177 if ( req.query.json ) {
178 resp.writeHead( 400, { "content-type": "application/json" } );
179 resp.end( "{ \"code\": 40, \"message\": \"Bad Request\" }" );
181 resp.writeHead( 400 );
182 resp.end( "plain text message" );
185 headers: function( req, resp ) {
187 "Sample-Header": "Hello World",
189 "Sample-Header2": "Hello World 2",
190 "List-Header": "Item 1",
191 "list-header": "Item 2",
192 "constructor": "prototype collision (constructor)"
195 resp.writeHead( 200, headers );
196 req.query.keys.split( "|" ).forEach( function( key ) {
197 if ( key.toLowerCase() in req.headers ) {
198 resp.write( `${ key }: ${ req.headers[ key.toLowerCase() ] }\n` );
203 echoData: function( req, resp, next ) {
204 getBody( req ).then( function( body ) {
208 echoQuery: function( req, resp ) {
209 resp.end( req.parsed.search.slice( 1 ) );
211 echoMethod: function( req, resp ) {
212 resp.end( req.method );
214 echoHtml: function( req, resp, next ) {
215 resp.writeHead( 200, { "Content-Type": "text/html" } );
216 resp.write( `<div id='method'>${ req.method }</div>` );
217 resp.write( `<div id='query'>${ req.parsed.search.slice( 1 ) }</div>` );
218 getBody( req ).then( function( body ) {
219 resp.write( `<div id='data'>${ body }</div>` );
223 etag: function( req, resp ) {
224 const hash = Number( req.query.ts ).toString( 36 );
225 const etag = `W/"${ hash }"`;
226 if ( req.headers[ "if-none-match" ] === etag ) {
227 resp.writeHead( 304 );
231 resp.writeHead( 200, {
236 ims: function( req, resp ) {
237 const ts = req.query.ts;
238 if ( req.headers[ "if-modified-since" ] === ts ) {
239 resp.writeHead( 304 );
243 resp.writeHead( 200, {
248 status: function( req, resp ) {
249 resp.writeHead( Number( req.query.code ) );
252 testHTML: function( req, resp ) {
253 resp.writeHead( 200, { "Content-Type": "text/html" } );
254 const body = readFileSync(
255 `${ __dirname }/data/test.include.html`
258 .replace( /{{baseURL}}/g, req.query.baseURL );
261 cspFrame: function( _req, resp ) {
262 resp.writeHead( 200, {
263 "Content-Type": "text/html",
264 "Content-Security-Policy": "default-src 'self'; require-trusted-types-for 'script'; " +
265 "report-uri /test/data/mock.php?action=cspLog"
267 const body = readFileSync( `${ __dirname }/data/csp.include.html` ).toString();
270 cspNonce: function( req, resp ) {
271 const testParam = req.query.test ?
272 `-${ req.query.test.replace( /[^a-z0-9]/gi, "" ) }` :
274 resp.writeHead( 200, {
275 "Content-Type": "text/html",
276 "Content-Security-Policy": "script-src 'nonce-jquery+hardcoded+nonce'; " +
277 "report-uri /test/data/mock.php?action=cspLog"
279 const body = readFileSync(
280 `${ __dirname }/data/csp-nonce${ testParam }.html` ).toString();
283 cspAjaxScript: function( _req, resp ) {
284 resp.writeHead( 200, {
285 "Content-Type": "text/html",
286 "Content-Security-Policy": "script-src 'self'; " +
287 "report-uri /test/data/mock.php?action=cspLog"
289 const body = readFileSync(
290 `${ __dirname }/data/csp-ajax-script.html` ).toString();
293 cspLog: function( _req, resp ) {
295 resp.writeHead( 200 );
298 cspClean: function( _req, resp ) {
300 resp.writeHead( 200 );
303 trustedHtml: function( _req, resp ) {
304 resp.writeHead( 200, {
305 "Content-Type": "text/html",
306 "Content-Security-Policy": "require-trusted-types-for 'script'; " +
307 "report-uri /test/data/mock.php?action=cspLog"
309 const body = readFileSync( `${ __dirname }/data/trusted-html.html` ).toString();
312 trustedTypesAttributes: function( _req, resp ) {
313 resp.writeHead( 200, {
314 "Content-Type": "text/html",
315 "Content-Security-Policy": "require-trusted-types-for 'script'; " +
316 "report-uri /test/data/mock.php?action=cspLog"
318 const body = readFileSync(
319 `${ __dirname }/data/trusted-types-attributes.html` ).toString();
322 errorWithScript: function( req, resp ) {
323 if ( req.query.withScriptContentType ) {
324 resp.writeHead( 404, { "Content-Type": "application/javascript" } );
326 resp.writeHead( 404, { "Content-Type": "text/html; charset=UTF-8" } );
328 if ( req.query.callback ) {
329 resp.end( `${ cleanCallback( req.query.callback )
330 }( {"status": 404, "msg": "Not Found"} )` );
332 resp.end( "QUnit.assert.ok( false, \"Mock return erroneously executed\" );" );
337 "test/data/mock.php": function( req, resp, next ) {
338 if ( !mocks[ req.query.action ] ) {
339 resp.writeHead( 400 );
340 resp.end( "Invalid action query.\n" );
341 console.log( "Invalid action query:", req.method, req.url );
344 mocks[ req.query.action ]( req, resp, next );
346 "test/data/support/csp.log": function( _req, resp ) {
347 resp.writeHead( 200 );
350 "test/data/404.txt": function( _req, resp ) {
351 resp.writeHead( 404 );
357 * Connect-compatible middleware factory for mocking server responses.
358 * Used by Ajax tests run in Node.
360 function MockserverMiddlewareFactory() {
363 * @param {http.IncomingMessage} req
364 * @param {http.ServerResponse} resp
365 * @param {Function} next Continue request handling
367 return function( req, resp, next ) {
368 const parsed = url.parse( req.url, /* parseQuery */ true );
369 let path = parsed.pathname;
370 const query = parsed.query;
371 const subReq = Object.assign( Object.create( req ), {
376 if ( /^\/?test\/data\/mock.php\/?/.test( path ) ) {
378 // Support REST-like Apache PathInfo
379 path = "test\/data\/mock.php";
382 if ( !handlers[ path ] ) {
387 // console.log( "Mock handling", req.method, parsed.href );
388 handlers[ path ]( subReq, resp, next );
392 function getBody( req ) {
393 return req.method !== "POST" ?
394 Promise.resolve( "" ) :
400 function getMultiPartContent( req ) {
401 return new Promise( function( resolve ) {
402 if ( req.method !== "POST" ) {
407 const form = new multiparty.Form();
408 form.parse( req, function( _err, fields, files ) {
409 resolve( { fields, files } );
414 module.exports = MockserverMiddlewareFactory;