From 15a8f79e15a69ad4cec8365cb5d86cd731ba1953 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 24 Sep 2024 08:32:59 +0200 Subject: [PATCH] Fix (tests): Prevent crashing the manual test server when reading a non-existing file due to an "ERR_HTTP_HEADERS_SENT" Node.js error. --- .../lib/utils/manual-tests/createserver.js | 15 +- .../tests/utils/manual-tests/createserver.js | 283 +++++++++++++++++- 2 files changed, 293 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js index 1dcd59108..4baa9a4c3 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js @@ -59,17 +59,19 @@ export default function createManualTestServer( sourcePath, port = 8125, onCreat } function onRequest( sourcePath, request, response ) { - response.writeHead( 200, { - 'Content-Type': getContentType( request.url.endsWith( '/' ) ? '.html' : path.extname( request.url ) ) - } ); + const contentType = getContentType( request.url.endsWith( '/' ) ? '.html' : path.extname( request.url ) ); // Ignore a 'favicon' request. if ( request.url === '/favicon.ico' ) { + response.writeHead( 200, { 'Content-Type': contentType } ); + return response.end( null, 'utf-8' ); } // Generate index.html with list of the tests. if ( request.url === '/' ) { + response.writeHead( 200, { 'Content-Type': contentType } ); + return response.end( generateIndex( sourcePath ), 'utf-8' ); } @@ -79,7 +81,9 @@ function onRequest( sourcePath, request, response ) { const url = request.url.replace( /\?.+$/, '' ); const content = fs.readFileSync( path.join( sourcePath, url ) ); - response.end( content, 'utf-8' ); + response.writeHead( 200, { 'Content-Type': contentType } ); + + return response.end( content, 'utf-8' ); } catch ( error ) { logger().error( `[Server] Cannot find file '${ request.url }'.` ); @@ -101,6 +105,7 @@ function getContentType( fileExtension ) { return 'text/css'; case '.json': + case '.map': return 'application/json'; case '.png': @@ -152,6 +157,8 @@ function generateIndex( sourcePath ) { testList += ''; } + testList += ''; + const headerHtml = '

CKEditor 5 manual tests

'; return combine( viewTemplate, headerHtml, testList ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js index 4abfaf93c..3bf525743 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js @@ -5,12 +5,18 @@ import http from 'http'; import readline from 'readline'; +import fs from 'fs'; +import { globSync } from 'glob'; +import combine from 'dom-combiner'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { logger } from '@ckeditor/ckeditor5-dev-utils'; import createManualTestServer from '../../../lib/utils/manual-tests/createserver.js'; -vi.mock( '@ckeditor/ckeditor5-dev-utils' ); vi.mock( 'readline' ); +vi.mock( 'fs' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'glob' ); +vi.mock( 'dom-combiner' ); describe( 'createManualTestServer()', () => { let loggerStub, server; @@ -86,4 +92,279 @@ describe( 'createManualTestServer()', () => { expect( vi.mocked( readline ).createInterface ).toHaveBeenCalledOnce(); expect( readlineInterface.on ).toHaveBeenCalledExactlyOnceWith( 'SIGINT', expect.any( Function ) ); } ); + + describe( 'request handler', () => { + beforeEach( () => { + createManualTestServer( 'workspace/build/.manual-tests' ); + } ); + + it( 'should handle a request for a favicon (`/favicon.ico`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/favicon.ico' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/x-icon' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( null, 'utf-8' ); + } ); + + it( 'should handle a root request (`/`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( '' ); + vi.mocked( globSync ).mockReturnValue( [ + '/build/.manual-tests/ckeditor5-foo/tests/manual/test-1.html', + '/build/.manual-tests/ckeditor5-foo/tests/manual/test-2.html', + '/build/.manual-tests/ckeditor5-bar/tests/manual/test-3.html', + '/build/.manual-tests/ckeditor5-bar/tests/manual/test-4.html' + ] ); + vi.mocked( combine ).mockReturnValue( 'Generated index.' ); + + serverCallback( request, response ); + + const expectedTestList = + ''; + + expect( vi.mocked( combine ) ).toHaveBeenCalledExactlyOnceWith( + '', + expect.stringContaining( 'CKEditor 5 manual tests' ), + expectedTestList + ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/html' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'Generated index.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.html`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.html' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/html' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.js`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.js' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/javascript' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.json`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.json' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'application/json' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.js.map`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.js.map' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'application/json' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.css`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.css' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/css' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.png`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.png' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/png' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.jpg`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.jpg' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/jpg' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a non-existing resource', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.jpg' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( logger ).mockReturnValue( { + error: vi.fn() + } ); + + vi.mocked( fs ).readFileSync.mockImplementation( () => { + const error = new Error( 'A resource does not exist' ); + error.code = 'ENOENT'; + + throw error; + } ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 404 ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining( 'Sorry, check with the site admin for error: ENOENT' ), + 'utf-8' + ); + } ); + } ); } );