refactor(backend-dynamic-feature-service): all-in-one feature integration tests
Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
@@ -76,6 +76,8 @@
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-app-backend": "workspace:^",
|
||||
"triple-beam": "^1.4.1",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
!dist
|
||||
!node_modules
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"test-backend": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"secretValue"
|
||||
],
|
||||
"properties": {
|
||||
"secretValue": {
|
||||
"type": "string",
|
||||
"visibility": "secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
const { dynamicPluginsServiceRef } = require('../../../../../manager');
|
||||
var backendPluginApi = require('@backstage/backend-plugin-api');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const privateDep = require('private-dep-with-frontend-plugin-index-path');
|
||||
|
||||
const testPlugin = backendPluginApi.createBackendPlugin({
|
||||
pluginId: "test",
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
http: backendPluginApi.coreServices.httpRouter,
|
||||
logger: backendPluginApi.coreServices.rootLogger,
|
||||
discovery: backendPluginApi.coreServices.discovery,
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
metadata: backendPluginApi.coreServices.pluginMetadata,
|
||||
},
|
||||
async init({
|
||||
http,
|
||||
logger,
|
||||
discovery,
|
||||
dynamicPlugins,
|
||||
metadata,
|
||||
}) {
|
||||
logger.info("This secret value should be hidden by the dynamic-plugin-aware logger: AVerySecretValue");
|
||||
const externalBaseUrl = await discovery.getExternalBaseUrl(metadata.getId());
|
||||
const router = express.Router();
|
||||
const frontendPluginsIndexPath = privateDep.frontendPluginsIndexPath;
|
||||
const frontendPluginManifests = Object.fromEntries(dynamicPlugins.frontendPlugins().map(fp => {
|
||||
const pluginScannedPackage = dynamicPlugins.getScannedPackage(fp);
|
||||
const pkgDistLocation = path.resolve(
|
||||
url.fileURLToPath(pluginScannedPackage.location),
|
||||
'dist',
|
||||
);
|
||||
router.use(`/${frontendPluginsIndexPath}/${fp.name}`, express.static(pkgDistLocation))
|
||||
return [fp.name, `${externalBaseUrl}/${frontendPluginsIndexPath}/${fp.name}/mf-manifest.json`]
|
||||
}));
|
||||
router.get(`/${frontendPluginsIndexPath}`, (req, res) => {
|
||||
res.status(200).json(frontendPluginManifests);
|
||||
});
|
||||
http.use(router);
|
||||
http.addAuthPolicy({
|
||||
path: `/`,
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.default = testPlugin;
|
||||
//# sourceMappingURL=alpha.cjs.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
const frontendPluginsIndexPath = 'frontend-plugins';
|
||||
|
||||
exports.frontendPluginsIndexPath = frontendPluginsIndexPath;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "private-dep-with-frontend-plugin-index-path",
|
||||
"version": "0.0.0",
|
||||
"description": "private dependency of the test backend plugin",
|
||||
"main": "index.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "plugin-test-backend-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic backend module that exposes the dynamic frontend plugins to an endpoint.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "test",
|
||||
"pluginPackages": [
|
||||
"plugin-test",
|
||||
"plugin-test-backend"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"default": "./dist/index.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.cjs.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"private-dep-with-frontend-plugin-index-path": "0.0.0"
|
||||
},
|
||||
"bundleDependencies": true
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"test-frontend": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"frontendValue"
|
||||
],
|
||||
"properties": {
|
||||
"frontendValue": {
|
||||
"type": "string",
|
||||
"visibility": "frontend"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "backstage__plugin_test",
|
||||
"name": "backstage__plugin_test",
|
||||
"metaData": {
|
||||
"name": "backstage__plugin_test",
|
||||
"type": "app",
|
||||
"buildInfo": {
|
||||
"buildVersion": "0.0.0",
|
||||
"buildName": "@backstage/plugin-test"
|
||||
},
|
||||
"remoteEntry": {
|
||||
"name": "remoteEntry.js",
|
||||
"path": "",
|
||||
"type": "global"
|
||||
},
|
||||
"types": {
|
||||
"path": "",
|
||||
"name": "",
|
||||
"zip": "",
|
||||
"api": ""
|
||||
},
|
||||
"globalName": "backstage__plugin_test",
|
||||
"pluginVersion": "0.0.0",
|
||||
"publicPath": "auto"
|
||||
},
|
||||
"shared": [],
|
||||
"remotes": [],
|
||||
"exposes": []
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
(function doNothing(){})();
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "plugin-test-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic, module-federation-based, Backstage frontend plugin that does nothing",
|
||||
"backstage": {
|
||||
"role": "frontend-dynamic-container",
|
||||
"pluginId": "test",
|
||||
"pluginPackages": [
|
||||
"plugin-test"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"main": "remote-entry.js"
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
throw new Error("False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader");
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"version": "0.0.0",
|
||||
"description": "dummy backstage package that should be skipped by the ComonJSLoduleLoader",
|
||||
"main": "index.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
startTestBackend,
|
||||
mockServices,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { dynamicPluginsFeatureLoader } from './features';
|
||||
import { DynamicPlugin, dynamicPluginsServiceRef } from '../manager';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import {
|
||||
BackendFeature,
|
||||
createBackendPlugin,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CommonJSModuleLoader } from '../loader/CommonJSModuleLoader';
|
||||
import * as winston from 'winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils';
|
||||
|
||||
async function jestFreeTypescriptAwareModuleLoader(
|
||||
logger: LoggerService,
|
||||
dontBootstrap: boolean = false,
|
||||
) {
|
||||
const loader = new CommonJSModuleLoader(logger);
|
||||
(loader as any).module = await loader.load('node:module');
|
||||
loader.load(path.resolve(__dirname, '../../../cli/config/nodeTransform.cjs'));
|
||||
if (dontBootstrap) {
|
||||
loader.bootstrap = async () => {};
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
class MockedTransport extends winston.transports.Console {
|
||||
readonly logs: string[] = [];
|
||||
|
||||
public log(info: any, callback: () => void) {
|
||||
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
|
||||
this.logs.push(info[MESSAGE]);
|
||||
}
|
||||
super.log!(info, callback);
|
||||
}
|
||||
public logv(info: any, callback: () => void) {
|
||||
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
|
||||
this.logs.push(info[MESSAGE]);
|
||||
}
|
||||
super.log!(info, callback);
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicPluginLister {
|
||||
readonly loadedPlugins: DynamicPlugin[] = [];
|
||||
feature(): BackendFeature {
|
||||
// eslint-disable-next-line consistent-this
|
||||
const that = this;
|
||||
return createBackendPlugin({
|
||||
pluginId: 'dynamicPluginsLister',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
},
|
||||
async init({ dynamicPlugins }) {
|
||||
that.loadedPlugins.push(...dynamicPlugins.plugins(true));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('dynamicPluginsFeatureLoader', () => {
|
||||
const dynamicPluginsRootDirectory = resolvePath(
|
||||
__dirname,
|
||||
'__fixtures__/dynamic-plugins-root',
|
||||
);
|
||||
|
||||
// A dummy `@backstage/backend-plugin-api` package which throws an error is available inside the test fixtures,
|
||||
// in a `node_modules` folder which is a sibling of the `dynamic-plugins-root`.
|
||||
// This test demonstrates how, without the skipping logic implemented in the {@link CommonJSModelLoader},
|
||||
// this dummy package would be loaded by the backend dynamic plugins instead of the one of the backstage root.
|
||||
it('should fail because the model loader is not skipping modules living in unexpected locations.', async () => {
|
||||
const dynamicPLuginsLister = new DynamicPluginLister();
|
||||
const mockedTransport = new MockedTransport();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: logger =>
|
||||
jestFreeTypescriptAwareModuleLoader(logger, true),
|
||||
transports: [mockedTransport],
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
dynamicPLuginsLister.feature(),
|
||||
],
|
||||
});
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
expect.stringMatching(
|
||||
"error: an error occurred while loading dynamic backend plugin 'plugin-test-backend-dynamic' from '.*/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic",
|
||||
),
|
||||
);
|
||||
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
|
||||
{
|
||||
name: 'plugin-test-backend-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
failure:
|
||||
'Error: False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader',
|
||||
},
|
||||
expect.anything(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should load and show the 2 dynamic plugins in a list of dynamic plugins returned by a static backend plugin', async () => {
|
||||
const dynamicPLuginsLister = new DynamicPluginLister();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
dynamicPLuginsLister.feature(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
|
||||
{
|
||||
installer: {
|
||||
kind: 'new',
|
||||
},
|
||||
name: 'plugin-test-backend-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'plugin-test-dynamic',
|
||||
platform: 'web',
|
||||
role: 'frontend-dynamic-container',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should redact the secret config values of dynamic plugin config schemas in logs', async () => {
|
||||
const mockedTransport = new MockedTransport();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
'test-backend': {
|
||||
secretValue: 'AVerySecretValue',
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
transports: [mockedTransport],
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
'info: Found 1 new secrets in config that will be redacted {"service":"backstage"}',
|
||||
);
|
||||
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
'info: This secret value should be hidden by the dynamic-plugin-aware logger: *** {"service":"backstage"}',
|
||||
);
|
||||
});
|
||||
|
||||
const mockAppDir = createMockDirectory();
|
||||
overridePackagePathResolution({
|
||||
packageName: 'app',
|
||||
path: mockAppDir.path,
|
||||
});
|
||||
|
||||
it('should inject frontend config values of dynamic frontend plugin config schemas to the frontend application', async () => {
|
||||
mockAppDir.setContent({
|
||||
'package.json': '{}',
|
||||
dist: {
|
||||
static: {},
|
||||
'index.html.tmpl': '<head></head>',
|
||||
'.config-schema.json': `
|
||||
{
|
||||
"backstageConfigSchemaVersion": 1,
|
||||
"schemas": []
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
'test-frontend': {
|
||||
frontendValue: 'AFrontendValue',
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
import('@backstage/plugin-app-backend/alpha'),
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetch(`http://localhost:${server.port()}`).then(res => res.text()),
|
||||
).resolves.toBe(`<head>
|
||||
<script type="backstage.io/config">
|
||||
[
|
||||
{
|
||||
"context": "app",
|
||||
"deprecatedKeys": [],
|
||||
"data": {
|
||||
"test-frontend": {
|
||||
"frontendValue": "AFrontendValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
</head>`);
|
||||
});
|
||||
|
||||
it('should access the module federation assets of the frontend plugin through the backend plugin', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const list = await fetch(
|
||||
`http://localhost:${server.port()}/api/test/frontend-plugins`,
|
||||
);
|
||||
expect(list.ok).toBe(true);
|
||||
expect(await list.json()).toEqual({
|
||||
'plugin-test-dynamic': `http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
|
||||
});
|
||||
|
||||
const manifest = await fetch(
|
||||
`http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
|
||||
);
|
||||
expect(manifest.ok).toBe(true);
|
||||
expect(await manifest.json()).toMatchObject({
|
||||
exposes: [],
|
||||
id: 'backstage__plugin_test',
|
||||
name: 'backstage__plugin_test',
|
||||
metaData: {
|
||||
buildInfo: {
|
||||
buildName: '@backstage/plugin-test',
|
||||
buildVersion: '0.0.0',
|
||||
},
|
||||
globalName: 'backstage__plugin_test',
|
||||
name: 'backstage__plugin_test',
|
||||
pluginVersion: '0.0.0',
|
||||
publicPath: 'auto',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ export const dynamicPluginsFrontendSchemas = createBackendModule({
|
||||
config.getOptionalString('app.packageName') ?? 'app';
|
||||
const appDistDir = resolvePackagePath(appPackageName, 'dist');
|
||||
const compiledConfigSchema = await loadCompiledConfigSchema(appDistDir);
|
||||
// TODO(davidfestal): Add dynamic pliugin config schemas even if the compiled schemas are empty.
|
||||
if (compiledConfigSchema) {
|
||||
configSchemaExtension.setConfigSchema(
|
||||
(await schemas.addDynamicPluginsSchemas(compiledConfigSchema))
|
||||
|
||||
@@ -3712,6 +3712,7 @@ __metadata:
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/config-loader": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-app-backend": "workspace:^"
|
||||
"@backstage/plugin-app-node": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend": "workspace:^"
|
||||
@@ -3729,6 +3730,7 @@ __metadata:
|
||||
express: ^4.17.1
|
||||
fs-extra: ^11.2.0
|
||||
lodash: ^4.17.21
|
||||
triple-beam: ^1.4.1
|
||||
wait-for-expect: ^3.0.2
|
||||
winston: ^3.2.1
|
||||
languageName: unknown
|
||||
|
||||
Reference in New Issue
Block a user