cli: add option to reject network requets in frontend tests
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added a new `"rejectFrontendNetworkRequests"` configuration flag that can be set in the `"jest"` field in the root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jest": {
|
||||
"rejectFrontendNetworkRequests": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This flag causes rejection of any form of network requests that are attempted to be made in frontend or common package tests. This flag can only be set in the root `package.json` and can not be overridden in individual package configurations.
|
||||
@@ -133,6 +133,9 @@
|
||||
"sort-package-json": "^2.8.0",
|
||||
"typescript": "~5.2.0"
|
||||
},
|
||||
"jest": {
|
||||
"rejectFrontendNetworkRequests": true
|
||||
},
|
||||
"packageManager": "yarn@3.8.1",
|
||||
"engines": {
|
||||
"node": "18 || 20"
|
||||
|
||||
@@ -98,7 +98,9 @@
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"cross-env": "^7.0.0"
|
||||
"axios": "^1.7.7",
|
||||
"cross-env": "^7.0.0",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"bundled": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import http from 'http';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import https from 'https';
|
||||
|
||||
const errorMsg = 'Network requests are not allowed in tests';
|
||||
|
||||
// These test relates to the @backstage/cli Jest configuration. It makes sure
|
||||
// that network requests are properly rejected in JSDom environments.
|
||||
|
||||
describe('without msw', () => {
|
||||
it('should reject network requests', async () => {
|
||||
await expect(fetch('https://example.com')).rejects.toThrow(errorMsg);
|
||||
await expect(axios('https://example.com')).rejects.toThrow(errorMsg);
|
||||
expect(() => http.get('http://example.com')).toThrow(errorMsg);
|
||||
expect(() => https.get('https://example.com')).toThrow(errorMsg);
|
||||
await expect(
|
||||
new Promise(resolve => {
|
||||
const ws = new WebSocket('ws://example.com');
|
||||
ws.addEventListener('error', () => resolve('error'));
|
||||
}),
|
||||
).resolves.toBe('error');
|
||||
expect(typeof EventSource).toBe('undefined');
|
||||
expect(() => new XMLHttpRequest()).toThrow(errorMsg);
|
||||
});
|
||||
});
|
||||
|
||||
// This makes sure that MSW mocks still work as expected
|
||||
|
||||
describe('with msw', () => {
|
||||
const server = setupServer();
|
||||
registerMswTestHooks(server);
|
||||
|
||||
it('should mock network requests', async () => {
|
||||
server.use(
|
||||
rest.get('http://example.com', (_, res, ctx) =>
|
||||
res(ctx.json({ ok: true })),
|
||||
),
|
||||
rest.get('https://example.com', (_, res, ctx) =>
|
||||
res(ctx.json({ ok: true })),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
fetch('https://example.com').then(res => res.json()),
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
axios('https://example.com').then(res => res.data),
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
new Promise(resolve => {
|
||||
const req = http.get('http://example.com');
|
||||
req.on('response', res => {
|
||||
res.on('data', data => {
|
||||
resolve(JSON.parse(data.toString()));
|
||||
});
|
||||
});
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
new Promise(resolve => {
|
||||
const req = https.get('https://example.com');
|
||||
req.on('response', res => {
|
||||
res.on('data', data => {
|
||||
resolve(JSON.parse(data.toString()));
|
||||
});
|
||||
});
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://example.com');
|
||||
xhr.onload = () => {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
};
|
||||
xhr.send();
|
||||
}),
|
||||
).resolves.toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
+46
-24
@@ -23,6 +23,14 @@ const paths = require('@backstage/cli-common').findPaths(process.cwd());
|
||||
|
||||
const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs'];
|
||||
|
||||
const FRONTEND_ROLES = [
|
||||
'frontend',
|
||||
'web-library',
|
||||
'common-library',
|
||||
'frontend-plugin',
|
||||
'frontend-plugin-module',
|
||||
];
|
||||
|
||||
const envOptions = {
|
||||
oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS),
|
||||
};
|
||||
@@ -121,24 +129,13 @@ const transformIgnorePattern = [
|
||||
|
||||
// Provides additional config that's based on the role of the target package
|
||||
function getRoleConfig(role) {
|
||||
switch (role) {
|
||||
case 'frontend':
|
||||
case 'web-library':
|
||||
case 'common-library':
|
||||
case 'frontend-plugin':
|
||||
case 'frontend-plugin-module':
|
||||
return { testEnvironment: require.resolve('jest-environment-jsdom') };
|
||||
case 'cli':
|
||||
case 'backend':
|
||||
case 'node-library':
|
||||
case 'backend-plugin':
|
||||
case 'backend-plugin-module':
|
||||
default:
|
||||
return { testEnvironment: require.resolve('jest-environment-node') };
|
||||
if (FRONTEND_ROLES.includes(role)) {
|
||||
return { testEnvironment: require.resolve('jest-environment-jsdom') };
|
||||
}
|
||||
return { testEnvironment: require.resolve('jest-environment-node') };
|
||||
}
|
||||
|
||||
async function getProjectConfig(targetPath, extraConfig) {
|
||||
async function getProjectConfig(targetPath, extraConfig, extraOptions) {
|
||||
const configJsPath = path.resolve(targetPath, 'jest.config.js');
|
||||
const configTsPath = path.resolve(targetPath, 'jest.config.ts');
|
||||
// If the package has it's own jest config, we use that instead.
|
||||
@@ -232,6 +229,17 @@ async function getProjectConfig(targetPath, extraConfig) {
|
||||
|
||||
options.setupFilesAfterEnv = options.setupFilesAfterEnv || [];
|
||||
|
||||
if (
|
||||
extraOptions.rejectFrontendNetworkRequests &&
|
||||
FRONTEND_ROLES.includes(pkgJson.backstage?.role)
|
||||
) {
|
||||
// By adding this first we ensure that it's possible to for example override
|
||||
// fetch with a mock in a custom setup file
|
||||
options.setupFilesAfterEnv.unshift(
|
||||
require.resolve('./jestRejectNetworkRequests.js'),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.testEnvironment === require.resolve('jest-environment-jsdom')) {
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/1724
|
||||
options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill'));
|
||||
@@ -276,21 +284,31 @@ async function getRootConfig() {
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
|
||||
};
|
||||
|
||||
const { rejectFrontendNetworkRequests, ...rootOptions } =
|
||||
rootPkgJson.jest ?? {};
|
||||
const extraRootOptions = {
|
||||
rejectFrontendNetworkRequests,
|
||||
};
|
||||
|
||||
const workspacePatterns =
|
||||
rootPkgJson.workspaces && rootPkgJson.workspaces.packages;
|
||||
|
||||
// Check if we're running within a specific monorepo package. In that case just get the single project config.
|
||||
if (!workspacePatterns || paths.targetRoot !== paths.targetDir) {
|
||||
return getProjectConfig(paths.targetDir, {
|
||||
...baseCoverageConfig,
|
||||
...(rootPkgJson.jest ?? {}),
|
||||
});
|
||||
return getProjectConfig(
|
||||
paths.targetDir,
|
||||
{
|
||||
...baseCoverageConfig,
|
||||
...rootOptions,
|
||||
},
|
||||
extraRootOptions,
|
||||
);
|
||||
}
|
||||
|
||||
const globalRootConfig = { ...baseCoverageConfig };
|
||||
const globalProjectConfig = {};
|
||||
|
||||
for (const [key, value] of Object.entries(rootPkgJson.jest ?? {})) {
|
||||
for (const [key, value] of Object.entries(rootOptions)) {
|
||||
if (projectConfigKeys.includes(key)) {
|
||||
globalProjectConfig[key] = value;
|
||||
} else {
|
||||
@@ -321,10 +339,14 @@ async function getRootConfig() {
|
||||
testScript?.includes('backstage-cli test') ||
|
||||
testScript?.includes('backstage-cli package test');
|
||||
if (testScript && isSupportedTestScript) {
|
||||
return await getProjectConfig(projectPath, {
|
||||
...globalProjectConfig,
|
||||
displayName: packageData.name,
|
||||
});
|
||||
return await getProjectConfig(
|
||||
projectPath,
|
||||
{
|
||||
...globalProjectConfig,
|
||||
displayName: packageData.name,
|
||||
},
|
||||
extraRootOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const errorMessage = 'Network requests are not allowed in tests';
|
||||
|
||||
const origHttpAgent = http.globalAgent;
|
||||
const origHttpsAgent = https.globalAgent;
|
||||
const origFetch = global.fetch;
|
||||
const origXMLHttpRequest = global.fetch;
|
||||
|
||||
http.globalAgent = new http.Agent({
|
||||
lookup() {
|
||||
throw new Error(errorMessage);
|
||||
},
|
||||
});
|
||||
|
||||
https.globalAgent = new https.Agent({
|
||||
lookup() {
|
||||
throw new Error(errorMessage);
|
||||
},
|
||||
});
|
||||
|
||||
if (global.fetch) {
|
||||
global.fetch = async () => {
|
||||
throw new Error(errorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
global.XMLHttpRequest = class {
|
||||
constructor() {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Reset overrides after each suite to make sure we don't pollute the test environment
|
||||
afterAll(() => {
|
||||
http.globalAgent = origHttpAgent;
|
||||
https.globalAgent = origHttpsAgent;
|
||||
global.fetch = origFetch;
|
||||
global.XMLHttpRequest = origXMLHttpRequest;
|
||||
});
|
||||
@@ -21628,7 +21628,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:1.7.7, axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.7.4":
|
||||
"axios@npm:1.7.7, axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.7.7":
|
||||
version: 1.7.7
|
||||
resolution: "axios@npm:1.7.7"
|
||||
dependencies:
|
||||
@@ -26975,8 +26975,10 @@ __metadata:
|
||||
"@types/react-dom": "*"
|
||||
"@types/zen-observable": ^0.8.0
|
||||
"@vitejs/plugin-react": ^4.3.1
|
||||
axios: ^1.7.7
|
||||
cross-env: ^7.0.0
|
||||
history: ^5.0.0
|
||||
msw: ^1.0.0
|
||||
react: ^18.0.2
|
||||
react-dom: ^18.0.2
|
||||
react-router: ^6.3.0
|
||||
|
||||
Reference in New Issue
Block a user