Skip to content

Commit

Permalink
test(SSR e2e): case when a call to Backend timeouts (#19307)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Platonn authored Oct 3, 2024
1 parent ce5047c commit 23b72bb
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 73 deletions.
153 changes: 103 additions & 50 deletions projects/ssr-tests/src/ssr-testing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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({
Expand Down
75 changes: 52 additions & 23 deletions projects/ssr-tests/src/utils/proxy.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -34,24 +51,36 @@ export async function startBackendProxyServer(
): Promise<http.Server> {
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, () => {
Expand Down

0 comments on commit 23b72bb

Please sign in to comment.