Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add support for injecting cxConfig into ssr testing framework #18801

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env-cmdrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"local": {
"CX_BASE_URL": "https://localhost:9002"
},
"local-http": {
"CX_BASE_URL": "http://localhost:9002"
},
"ci": {
"CX_BASE_URL": "https://20.83.184.244:9002"
},
Expand Down
2 changes: 1 addition & 1 deletion ci-scripts/unit-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -e
set -o pipefail

EXCLUDE_APPLICATIONS=storefrontapp
EXCLUDE_APPLICATIONS=storefrontapp,ssr-tests
EXCLUDE_JEST=storefrontstyles,schematics,setup

echo "-----"
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"build:setup": "nx build setup --configuration production",
"build:ssr": "env-cmd --no-override -e dev,b2c,$SPA_ENV nx run storefrontapp:server:production",
"build:ssr:ci": "env-cmd -e ci,b2c,$SPA_ENV nx run storefrontapp:server:production",
"build:ssr:local-http": "env-cmd -e local-http,b2c,$SPA_ENV nx run storefrontapp:server:production",
"build:storefinder": "npm --prefix feature-libs/storefinder run build:schematics && nx build storefinder --configuration production",
"build:smartedit": "npm --prefix feature-libs/smartedit run build:schematics && nx build smartedit --configuration production",
"build:tracking": "npm --prefix feature-libs/tracking run build:schematics && nx build tracking --configuration production",
Expand Down Expand Up @@ -98,6 +99,8 @@
"serve:ssr": "node dist/storefrontapp-server/main.js",
"serve:ssr:ci": "NODE_TLS_REJECT_UNAUTHORIZED=0 SSR_TIMEOUT=0 node dist/storefrontapp-server/main.js",
"serve:ssr:dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 node dist/storefrontapp-server/main.js",
"test:ssr": "env-cmd -e dev nx test ssr-tests",
"test:ssr:ci": "env-cmd -e ci nx test ssr-tests",
"prerender": "nx run storefrontapp:prerender --routes-file projects/storefrontapp/prerender.txt",
"prerender:dev": "env-cmd --no-override -e dev,$SPA_ENV cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 nx run storefrontapp:prerender --routes-file projects/storefrontapp/prerender.txt",
"prerender:cds": "env-cmd --no-override -e cds nx run storefrontapp:prerender --routes-file projects/storefrontapp/prerender.txt",
Expand Down Expand Up @@ -193,6 +196,7 @@
"eslint-plugin-prefer-arrow": "^1.2.3",
"fs-extra": "^11.1.1",
"glob": "^7.1.6",
"http-proxy": "^1.18.1",
"http-server": "^14.1.1",
"i18n-lint": "^1.1.0",
"jasmine-core": "~4.6.0",
Expand Down Expand Up @@ -235,4 +239,4 @@
"@nguniversal/builders/browser-sync/qs": "6.2.4",
"@nguniversal/builders/browser-sync/ua-parser-js": "1.0.33"
}
}
}
3 changes: 3 additions & 0 deletions projects/schematics/src/dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"parse5": "^7.1.2",
"typescript": "^5.2.2"
},
"ssr-tests": {
"http-proxy": "^1.18.1"
},
"storefrontapp-e2e-cypress": {},
"@spartacus/storefront": {
"@angular/common": "^17.0.5",
Expand Down
3 changes: 2 additions & 1 deletion projects/schematics/src/shared/utils/graph-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ export function kahnsAlgorithm(graph: Graph): string[] {
function createLibraryDependencyGraph(): Graph {
const skip = CORE_SPARTACUS_SCOPES.concat(
'storefrontapp-e2e-cypress',
'storefrontapp'
'storefrontapp',
'ssr-tests'
);

const spartacusLibraries = Object.keys(collectedDependencies).filter(
Expand Down
12 changes: 12 additions & 0 deletions projects/ssr-tests/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../.eslintrc.json",
"ignorePatterns": ["**/*.d.ts"],
"overrides": [
{
"files": ["*.ts"],
"rules": {
"no-console": "off"
}
}
]
}
1 change: 1 addition & 0 deletions projects/ssr-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssr.log
34 changes: 34 additions & 0 deletions projects/ssr-tests/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.json');
const { defaultTransformerOptions } = require('jest-preset-angular/presets');

/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
module.exports = {
preset: 'jest-preset-angular',
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
prefix: '<rootDir>/',
}),
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testMatch: ['**/+(*.)+(spec).+(ts)'],
transform: {
'^.+\\.(ts|js|mjs|html|svg)$': [
'jest-preset-angular',
{
...defaultTransformerOptions,
tsconfig: '<rootDir>/tsconfig.json',
},
],
},

collectCoverage: false,
coverageReporters: ['json', 'lcov', 'text', 'clover'],
coverageDirectory: '<rootDir>/../../coverage/ssr-tests',
coverageThreshold: {
global: {
statements: 90,
branches: 74,
functions: 90,
lines: 90,
},
},
};
21 changes: 21 additions & 0 deletions projects/ssr-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "ssr-tests",
"description": "Spartacus SSR Tests",
"keywords": [
"spartacus",
"ssr",
"tests"
],
"author": "SAP, Spartacus team",
"license": "Apache-2.0",
"private": true,
"scripts": {
"test": "../../node_modules/.bin/jest --config ./jest.config.js"
},
"dependencies": {
"tslib": "^2.6.2"
},
"peerDependencies": {
"http-proxy": "^1.18.1"
}
}
22 changes: 22 additions & 0 deletions projects/ssr-tests/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "ssr-tests",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "projects/ssr-tests/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["projects/ssr-tests/**/*.ts"]
}
},
"test-jest": {
"executor": "nx:run-commands",
"options": {
"command": "npm run test --verbose",
"cwd": "projects/ssr-tests"
}
}
},
"tags": ["type:util"]
}
9 changes: 9 additions & 0 deletions projects/ssr-tests/setup-jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import 'jest-preset-angular/setup-jest';
import 'zone.js';
86 changes: 86 additions & 0 deletions projects/ssr-tests/src/log.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Contains methods pertaining to reading, writing and asserting of the ssr log
* generated by a running ssr server for the sake of testing ssr.
*/

import * as fs from 'fs';

/**
* Path where SSR log file from server will be generated and read from.
*/
const SSR_LOG_PATH = './ssr.log';

/**
* Writes no characters to log to clear log file.
*/
export function clearSsrLogFile(): void {
fs.writeFileSync(SSR_LOG_PATH, '');
}

/**
* Returns all text in the log as a single string.
*/
export function getLogText(): string {
return fs.readFileSync(SSR_LOG_PATH).toString();
}

/**
* Reads log and returns messages as string array.
*/
export function getLogMessages(): string[] {
const data = fs.readFileSync(SSR_LOG_PATH).toString();
return (
data
.toString()
.split('\n')
// We're interested only in JSON logs from Spartacus SSR app.
// We ignore plain text logs coming from other sources, like `Node Express server listening on http://localhost:4200`
.filter((text: string) => text.charAt(0) === '{')
.map((text: any) => JSON.parse(text).message)
);
}

/**
* Check that log contains expected messages in string array.
* Fail test if log does not contain expected messages.
*/
export function assertMessages(expected: string[]): void {
const messages = getLogMessages();
for (const message of expected) {
expect(messages).toContain(message);
}
}

/**
* Check log every interval to see if log contains text.
* Keeps waiting until log contains text or test times out.
*/
export async function waitUntilLogContainsText(
text: string,
checkInterval = 500
): Promise<true> {
return new Promise((resolve) => {
if (doesLogContainText(text)) {
return resolve(true);
}
return setTimeout(
() => resolve(waitUntilLogContainsText(text)),
checkInterval
);
});
}

/**
* Returns true if log contains string.
*/
export function doesLogContainText(text: string): boolean {
const data = fs.readFileSync(SSR_LOG_PATH).toString();
return data.includes(text);
}
114 changes: 114 additions & 0 deletions projects/ssr-tests/src/proxy.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import * as http from 'http';
import * as httpProxy from 'http-proxy';

const proxy = (<any>httpProxy).createProxyServer({ secure: false });

/**
* Default settings to send http requests.
*/
const REQUEST_OPTIONS = {
host: 'localhost',
port: 4000,
};

interface ProxyOptions {
/**
* The url to reroute requests to.
*/
target: string;
/**
* Number of seconds to delay requests before sending.
*/
delay?: number;
/**
* Number of status code to set response to.
*/
throwStatus?: number;
}

/**
* Starts an http proxy server on port 9002 with the provided options.
*/
export async function startProxyServer(options: ProxyOptions) {
return new Promise((resolve) => {
const server = http.createServer((req: any, res: any) => {
const forwardRequest = () =>
proxy.web(req, res, { target: options.target });

if (options.throwStatus) {
proxy.on('proxyRes', (proxyRes: any) => {
proxyRes.statusCode = options.throwStatus;
});
}

if (options.delay) {
setTimeout(forwardRequest, options.delay);
} else {
forwardRequest();
}
});

server.listen(9002, () => {
resolve(server);
});
});
}

/**
* Send an http GET request to a given url.
*/
export async function sendRequest(path: string, cxConfig?: any) {
return new Promise((resolve, reject) => {
const req = http.get(
{
...REQUEST_OPTIONS,
path,
headers: {
Cookie: buildCxConfigE2ECookie(cxConfig),
},
},
(res: any) => {
const bodyChunks: any[] = [];

res
.on('data', (chunk: any) => {
bodyChunks.push(chunk);
})
.on('end', () => {
const buffer = Buffer.concat(bodyChunks);
const data = buffer.toJSON().data;
// TODO: Slice is hack to avoid hitting maximum call stack error
res.body = String.fromCharCode(...data.slice(0, 105000));
return resolve(res);
});
}
);

req.on('error', (e: Error) => {
reject(e);
});
});
}

/**
* * Builds a cookie string with key 'cxConfigE2E' and the given `cxConfig`
* * object as a value that is stringified and URI-encoded.
* *
* * Note: `TestConfigModule` of Spartacus will read this cookie
* * and inject the passed config chunk into Spartacus global config.
* */
function buildCxConfigE2ECookie(cxConfig?: object): string {
if (!cxConfig) {
return '{}';
}
const cookieKey = 'cxConfigE2E';
const cookieValue = encodeURIComponent(JSON.stringify(cxConfig));
return `${cookieKey}=${cookieValue}`;
}