From 23b72bb0749577b194d3a53c18fedcb334c8b0f5 Mon Sep 17 00:00:00 2001 From: Krzysztof Platis Date: Thu, 3 Oct 2024 14:00:26 +0200 Subject: [PATCH] test(SSR e2e): case when a call to Backend timeouts (#19307) Added a test case: when a call to Backend timeouts btw. changes: - replaced `callback` with `responseInterceptor` in ProxyUtils fixes https://jira.tools.sap/browse/CXSPA-8386 --- projects/ssr-tests/src/ssr-testing.spec.ts | 153 +++++++++++++------- projects/ssr-tests/src/utils/proxy.utils.ts | 75 +++++++--- 2 files changed, 155 insertions(+), 73 deletions(-) diff --git a/projects/ssr-tests/src/ssr-testing.spec.ts b/projects/ssr-tests/src/ssr-testing.spec.ts index d503bb2091a..b891d6bc25c 100644 --- a/projects/ssr-tests/src/ssr-testing.spec.ts +++ b/projects/ssr-tests/src/ssr-testing.spec.ts @@ -19,64 +19,115 @@ describe('SSR E2E', () => { describe('With SSR error handling', () => { describe('Common behavior', () => { - beforeEach(async () => { - await SsrUtils.startSsrServer(); - }); - - it('should receive success response with request', async () => { - backendProxy = await ProxyUtils.startBackendProxyServer({ - target: BACKEND_BASE_URL, + describe('With default SSR request timeout', () => { + beforeEach(async () => { + await SsrUtils.startSsrServer(); }); - const response: any = await HttpUtils.sendRequestToSsrServer({ - path: REQUEST_PATH, - }); - expect(response.statusCode).toEqual(200); - const logsMessages = LogUtils.getLogsMessages(); - expect(logsMessages).toContain(`Rendering started (${REQUEST_PATH})`); - expect(logsMessages).toContain( - `Request is waiting for the SSR rendering to complete (${REQUEST_PATH})` - ); - }); + it('should receive success response with request', async () => { + backendProxy = await ProxyUtils.startBackendProxyServer({ + target: BACKEND_BASE_URL, + }); + const response: any = await HttpUtils.sendRequestToSsrServer({ + path: REQUEST_PATH, + }); + expect(response.statusCode).toEqual(200); - it('should receive response with 404 when page does not exist', async () => { - backendProxy = await ProxyUtils.startBackendProxyServer({ - target: BACKEND_BASE_URL, + const logsMessages = LogUtils.getLogsMessages(); + expect(logsMessages).toContain(`Rendering started (${REQUEST_PATH})`); + expect(logsMessages).toContain( + `Request is waiting for the SSR rendering to complete (${REQUEST_PATH})` + ); }); - const response = await HttpUtils.sendRequestToSsrServer({ - path: REQUEST_PATH + 'not-existing-page', + + it('should receive response with 404 when page does not exist', async () => { + backendProxy = await ProxyUtils.startBackendProxyServer({ + target: BACKEND_BASE_URL, + }); + const response = await HttpUtils.sendRequestToSsrServer({ + path: REQUEST_PATH + 'not-existing-page', + }); + expect(response.statusCode).toEqual(404); }); - expect(response.statusCode).toEqual(404); - }); - it('should receive response with status 404 if HTTP error occurred when calling cms/pages API URL', async () => { - backendProxy = await ProxyUtils.startBackendProxyServer({ - target: BACKEND_BASE_URL, - callback: (proxyRes, req) => { - if (req.url?.includes('cms/pages')) { - proxyRes.statusCode = 404; - } - }, + it('should receive response with status 404 if HTTP error occurred when calling cms/pages API URL', async () => { + backendProxy = await ProxyUtils.startBackendProxyServer({ + target: BACKEND_BASE_URL, + responseInterceptor: ({ res, req, body }) => { + if (req.url?.includes('cms/pages')) { + res.statusCode = 404; + } + res.end(body); + }, + }); + const response = await HttpUtils.sendRequestToSsrServer({ + path: REQUEST_PATH, + }); + expect(response.statusCode).toEqual(404); }); - const response = await HttpUtils.sendRequestToSsrServer({ - path: REQUEST_PATH, + + it.skip('should receive response with status 500 if HTTP error occurred when calling other than cms/pages API URL', async () => { + backendProxy = await ProxyUtils.startBackendProxyServer({ + target: BACKEND_BASE_URL, + responseInterceptor: ({ res, req, body }) => { + if (req.url?.includes('cms/pages')) { + res.statusCode = 404; + } + res.end(body); + }, + }); + const response = await HttpUtils.sendRequestToSsrServer({ + path: REQUEST_PATH, + }); + expect(response.statusCode).toEqual(500); }); - expect(response.statusCode).toEqual(404); }); - it.skip('should receive response with status 500 if HTTP error occurred when calling other than cms/pages API URL', async () => { - backendProxy = await ProxyUtils.startBackendProxyServer({ - target: BACKEND_BASE_URL, - callback: (proxyRes, req) => { - if (req.url?.includes('cms/components')) { - proxyRes.statusCode = 404; - } - }, + describe('With long SSR request timeout', () => { + beforeEach(async () => { + // The SSR in the following tests might take longer than the default SSR request timeout. + // So to avoid getting a "CSR fallback" response, we're increasing the SSR request timeout to 20 seconds. + await SsrUtils.startSsrServer({ timeout: 20_000 }); }); - const response = await HttpUtils.sendRequestToSsrServer({ - path: REQUEST_PATH, + + it('should receive response with status 500 if HTTP call to backend timeouted', async () => { + /** + * We're configuring a custom time limit for the Backend API calls in Spartacus. + * This is to speed up the test. Otherwise, we would need to delay the call to /languages + * for 20 seconds (which is the default Backend Timeout in Spartacus) to get an error. + */ + const BACKEND_TIMEOUT_TIME_LIMIT = 4000; + const API_DELAY = BACKEND_TIMEOUT_TIME_LIMIT + 1; + + backendProxy = await ProxyUtils.startBackendProxyServer({ + target: BACKEND_BASE_URL, + responseInterceptor: ({ res, req, body }) => { + // Delay the response from Backend API, but only for for the /languages endpoint. + // We want Spartacus to consider this Backend API request as Timeouted (therefore failed). + if (req.url?.includes('languages')) { + setTimeout(() => res.end(body), API_DELAY); + } else { + res.end(body); + } + }, + }); + + const response = await HttpUtils.sendRequestToSsrServer({ + path: REQUEST_PATH, + testConfig: { + backend: { timeout: { server: BACKEND_TIMEOUT_TIME_LIMIT } }, + }, + }); + + expect(response.statusCode).toEqual(500); + + const logsMessages = LogUtils.getLogsMessages(); + expect(logsMessages.join('\n')).toMatch( + new RegExp( + `Error: Request to URL '[^']*\/languages' exceeded expected time of ${BACKEND_TIMEOUT_TIME_LIMIT}ms and was aborted` + ) + ); }); - expect(response.statusCode).toEqual(500); }); }); @@ -120,10 +171,11 @@ describe('SSR E2E', () => { async () => { backendProxy = await ProxyUtils.startBackendProxyServer({ target: BACKEND_BASE_URL, - callback: (proxyRes, req) => { + responseInterceptor: ({ res, req, body }) => { if (req.url?.includes('cms/pages')) { - proxyRes.statusCode = 404; + res.statusCode = 404; } + res.end(body); }, }); let response: HttpUtils.SsrResponse; @@ -193,10 +245,11 @@ describe('SSR E2E', () => { it('should receive response with status 500 even if HTTP error occurred when calling backend API URL', async () => { backendProxy = await ProxyUtils.startBackendProxyServer({ target: BACKEND_BASE_URL, - callback: (proxyRes, req) => { + responseInterceptor: ({ res, req, body }) => { if (req.url?.includes('cms/components')) { - proxyRes.statusCode = 400; + res.statusCode = 400; } + res.end(body); }, }); const response = await HttpUtils.sendRequestToSsrServer({ diff --git a/projects/ssr-tests/src/utils/proxy.utils.ts b/projects/ssr-tests/src/utils/proxy.utils.ts index 931ef6ccfd0..29d8be7d82a 100644 --- a/projects/ssr-tests/src/utils/proxy.utils.ts +++ b/projects/ssr-tests/src/utils/proxy.utils.ts @@ -16,14 +16,31 @@ interface ProxyOptions { * The url to reroute requests to. */ target: string; - /** - * Number of seconds to delay requests before sending. - */ - delay?: number; - /** - * Callback to be executed if the request to the target got a response. - */ - callback?: httpProxy.ProxyResCallback; + + responseInterceptor?: (params: { + /** + * The body of the response from the upstream server. + * + * Note: we're exposing separately from the `proxyRes` object for convenience + * (because extracting it manually from `proxyRes` is very cumbersome) + */ + body: string; + + /** + * The response from the upstream server. + */ + proxyRes: http.IncomingMessage; + + /** + * The request that was sent to the upstream server. + */ + req: http.IncomingMessage; + + /** + * The response that will be sent to the client. + */ + res: http.ServerResponse; + }) => void; } /** @@ -34,24 +51,36 @@ export async function startBackendProxyServer( ): Promise { const proxy = httpProxy.createProxyServer({ secure: false, + selfHandleResponse: !!options.responseInterceptor, }); + if (options.responseInterceptor) { + proxy.on('proxyRes', (proxyRes, req, res) => { + // We have to buffer the response body before passing it to the interceptor + const bodyBuffer: Buffer[] = []; + proxyRes.on('data', (chunk) => { + bodyBuffer.push(chunk); + }); + proxyRes.on('end', () => { + const body = Buffer.concat(bodyBuffer).toString(); + + // Pass the body to the interceptor + if (options.responseInterceptor) { + options.responseInterceptor({ + body, + proxyRes, + res, + req, + }); + } else { + res.end(body); + } + }); + }); + } + return new Promise((resolve) => { const server = http.createServer((req, res) => { - const forwardRequest = () => - proxy.web(req, res, { target: options.target }); - - if (options.callback) { - // Add one-time listener for the proxy response that stays until `proxyRes` event is triggered next time. - proxy.once('proxyRes', options.callback); - } - - if (options.delay) { - setTimeout(() => { - forwardRequest(); - }, options.delay); - } else { - forwardRequest(); - } + proxy.web(req, res, { target: options.target }); }); server.listen(9002, () => {