Merge pull request #20378 from backstage/rugvip/20-fixes

More fixes for node 20 test breakage + overridePackagePathResolution
This commit is contained in:
Patrik Oldsberg
2023-10-05 21:01:48 +02:00
committed by GitHub
25 changed files with 434 additions and 436 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added `/testUtils` entry point, with a utility for mocking resolve package paths as returned by `resolvePackagePath`.
+4
View File
@@ -10,6 +10,7 @@
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./testUtils": "./src/testUtils.ts",
"./package.json": "./package.json"
},
"typesVersions": {
@@ -17,6 +18,9 @@
"alpha": [
"src/alpha.ts"
],
"testUtils": [
"src/testUtils.ts"
],
"package.json": [
"package.json"
]
+14
View File
@@ -18,6 +18,12 @@ import { isChildPath } from '@backstage/cli-common';
import { NotAllowedError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
/** @internal */
export const packagePathMocks = new Map<
string,
(paths: string[]) => string | undefined
>();
/**
* Resolve a path relative to the root of a package directory.
* Additional path arguments are resolved relative to the package dir.
@@ -29,6 +35,14 @@ import { resolve as resolvePath } from 'path';
* @public
*/
export function resolvePackagePath(name: string, ...paths: string[]) {
const mockedResolve = packagePathMocks.get(name);
if (mockedResolve) {
const resolved = mockedResolve(paths);
if (resolved) {
return resolved;
}
}
const req =
typeof __non_webpack_require__ === 'undefined'
? require
+79
View File
@@ -0,0 +1,79 @@
/*
* 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 { packagePathMocks } from './paths';
import { posix as posixPath, resolve as resolvePath } from 'path';
/** @public */
export interface PackagePathResolutionOverride {
/** Restores the normal behavior of resolvePackagePath */
restore(): void;
}
/** @public */
export interface OverridePackagePathResolutionOptions {
/** The name of the package to mock the resolved path of */
packageName: string;
/** A replacement for the root package path */
path?: string;
/**
* Replacements for package sub-paths, each key must be an exact match of the posix-style path
* that is being resolved within the package.
*
* For example, code calling `resolvePackagePath('x', 'foo', 'bar')` would match only the following
* configuration: `overridePackagePathResolution({ packageName: 'x', paths: { 'foo/bar': baz } })`
*/
paths?: { [path in string]: string | (() => string) };
}
/**
* This utility helps you override the paths returned by `resolvePackagePath` for a given package.
*
* @public
*/
export function overridePackagePathResolution(
options: OverridePackagePathResolutionOptions,
): PackagePathResolutionOverride {
const name = options.packageName;
if (packagePathMocks.has(name)) {
throw new Error(
`Tried to override resolution for '${name}' more than once for package '${name}'`,
);
}
packagePathMocks.set(name, paths => {
const joinedPath = posixPath.join(...paths);
const localResolver = options.paths?.[joinedPath];
if (localResolver) {
return typeof localResolver === 'function'
? localResolver()
: localResolver;
}
if (options.path) {
return resolvePath(options.path, ...paths);
}
return undefined;
});
return {
restore() {
packagePathMocks.delete(name);
},
};
}
@@ -0,0 +1,26 @@
## API Report File for "@backstage/backend-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public
export function overridePackagePathResolution(
options: OverridePackagePathResolutionOptions,
): PackagePathResolutionOverride;
// @public (undocumented)
export interface OverridePackagePathResolutionOptions {
packageName: string;
path?: string;
paths?: {
[path in string]: string | (() => string);
};
}
// @public (undocumented)
export interface PackagePathResolutionOverride {
restore(): void;
}
// (No @packageDocumentation comment for this package)
```
@@ -91,7 +91,7 @@ describe('createMockDirectory', () => {
mockDir.addContent({
'b.txt': 'b',
b: {
[mockDir.resolve('b')]: {
'c.txt': 'c',
},
});
@@ -279,19 +279,18 @@ class MockDirectoryImpl {
const entries: MockEntry[] = [];
function traverse(node: MockDirectoryContent[string], path: string) {
const trimmedPath = path.startsWith('/') ? path.slice(1) : path; // trim leading slash
if (typeof node === 'string') {
entries.push({
type: 'file',
path: trimmedPath,
path,
content: Buffer.from(node, 'utf8'),
});
} else if (node instanceof Buffer) {
entries.push({ type: 'file', path: trimmedPath, content: node });
entries.push({ type: 'file', path, content: node });
} else {
entries.push({ type: 'dir', path: trimmedPath });
entries.push({ type: 'dir', path });
for (const [name, child] of Object.entries(node)) {
traverse(child, `${trimmedPath}/${name}`);
traverse(child, path ? `${path}/${name}` : name);
}
}
}
-1
View File
@@ -69,7 +69,6 @@
"@backstage/cli": "workspace:^",
"@backstage/types": "workspace:^",
"@types/supertest": "^2.0.8",
"mock-fs": "^5.2.0",
"msw": "^1.0.0",
"node-fetch": "^2.6.7",
"supertest": "^6.1.3"
@@ -14,40 +14,40 @@
* limitations under the License.
*/
import mockFs from 'mock-fs';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { findStaticAssets } from './findStaticAssets';
describe('findStaticAssets', () => {
const mockDir = createMockDirectory();
afterEach(() => {
mockFs.restore();
mockDir.clear();
});
it('should find assets', async () => {
mockFs({
'/test': {
'a.js': 'alert("hello")',
'a.js.map': '',
'b.js': 'b',
'b.js.map': '',
js: {
'd.js': 'd',
'd.js.map': '',
x: {
mockDir.setContent({
'a.js': 'alert("hello")',
'a.js.map': '',
'b.js': 'b',
'b.js.map': '',
js: {
'd.js': 'd',
'd.js.map': '',
x: {
'e.map': '',
y: {
'e.map': '',
y: {
z: {
'e.js': 'e',
'e.map': '',
z: {
'e.js': 'e',
'e.map': '',
},
},
},
},
styles: { 'c.css': 'body { color: red; }' },
},
styles: { 'c.css': 'body { color: red; }' },
});
const assets = await findStaticAssets('/test');
const assets = await findStaticAssets(mockDir.path);
expect(assets.length).toBe(5);
expect(assets.map(a => a.path)).toEqual(
expect.arrayContaining([
@@ -14,33 +14,36 @@
* limitations under the License.
*/
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import fetch from 'node-fetch';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import {
createMockDirectory,
mockServices,
startTestBackend,
} from '@backstage/backend-test-utils';
import { appPlugin } from './appPlugin';
import { createRootLogger } from '@backstage/backend-common';
import { overridePackagePathResolution } from '@backstage/backend-common/testUtils';
const mockDir = createMockDirectory();
overridePackagePathResolution({
packageName: 'app',
path: mockDir.path,
});
// Make sure root logger is initialized ahead of FS mock
createRootLogger();
describe('appPlugin', () => {
beforeEach(() => {
mockFs({
[resolvePath(process.cwd(), 'node_modules/app')]: {
'package.json': '{}',
dist: {
static: {},
'index.html': 'winning',
},
mockDir.setContent({
'package.json': '{}',
dist: {
static: {},
'index.html': 'winning',
},
});
});
afterEach(() => {
mockFs.restore();
});
it('boots', async () => {
const { server } = await startTestBackend({
features: [
@@ -372,7 +372,7 @@ describe('PlaceholderProcessor', () => {
() => {},
),
).rejects.toThrow(
/^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError \[ERR_INVALID_URL\]/,
/^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError/,
);
expect(reader.readUrl).not.toHaveBeenCalled();
-1
View File
@@ -89,7 +89,6 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/aws4": "^1.5.1",
"mock-fs": "^5.2.0",
"msw": "^1.0.0",
"supertest": "^6.1.3",
"ws": "^8.13.0"
@@ -13,8 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createMockDirectory } from '@backstage/backend-test-utils';
import { ServiceAccountStrategy } from './ServiceAccountStrategy';
import mockFs from 'mock-fs';
const mockDir = createMockDirectory({
content: {
'token.txt': 'in-cluster-token',
},
});
jest.mock('@kubernetes/client-node', () => ({
KubeConfig: class {
#loaded = false;
loadFromCluster() {
this.#loaded = true;
}
getCurrentUser() {
if (!this.#loaded) {
throw new Error('loadFromCluster not called');
}
return {
authProvider: {
config: {
get tokenFile() {
return mockDir.resolve('token.txt');
},
},
},
};
}
},
}));
describe('ServiceAccountStrategy', () => {
describe('#getCredential', () => {
@@ -32,16 +61,9 @@ describe('ServiceAccountStrategy', () => {
token: 'from config',
});
});
describe('when serviceAccountToken is absent from config', () => {
afterEach(() => {
mockFs.restore();
});
describe('when serviceAccountToken is absent from config', () => {
it('reads in-cluster token', async () => {
mockFs({
'/var/run/secrets/kubernetes.io/serviceaccount/token':
'in-cluster-token',
});
const strategy = new ServiceAccountStrategy();
const credential = await strategy.getCredential({
@@ -26,8 +26,17 @@ import {
rest,
} from 'msw';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import mockFs from 'mock-fs';
import {
createMockDirectory,
setupRequestMockHandlers,
} from '@backstage/backend-test-utils';
import { Config } from '@kubernetes/client-node';
const mockCertDir = createMockDirectory({
content: {
'ca.crt': 'MOCKCA',
},
});
const OBJECTS_TO_FETCH = new Set<ObjectToFetch>([
{
@@ -728,13 +737,7 @@ describe('KubernetesFetcher', () => {
expect(agent.options.ca).toBeUndefined();
});
describe('with a CA file on disk', () => {
afterEach(() => {
mockFs.restore();
});
it('should trust contents of specified caFile', async () => {
mockFs({
'/path/to/ca.crt': 'MOCKCA',
});
worker.use(
rest.get('https://localhost:9999/api/v1/pods', (req, res, ctx) =>
res(
@@ -752,7 +755,7 @@ describe('KubernetesFetcher', () => {
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {},
caFile: '/path/to/ca.crt',
caFile: mockCertDir.resolve('ca.crt'),
},
credential: { type: 'bearer token', token: 'token' },
objectTypesToFetch: new Set<ObjectToFetch>([
@@ -899,17 +902,18 @@ describe('KubernetesFetcher', () => {
describe('Backstage running on k8s', () => {
const initialHost = process.env.KUBERNETES_SERVICE_HOST;
const initialPort = process.env.KUBERNETES_SERVICE_PORT;
const initialCaPath = Config.SERVICEACCOUNT_CA_PATH;
afterEach(() => {
process.env.KUBERNETES_SERVICE_HOST = initialHost;
process.env.KUBERNETES_SERVICE_PORT = initialPort;
mockFs.restore();
Config.SERVICEACCOUNT_CA_PATH = initialCaPath;
});
it('makes in-cluster requests when cluster details has no token', async () => {
process.env.KUBERNETES_SERVICE_HOST = '10.10.10.10';
process.env.KUBERNETES_SERVICE_PORT = '443';
mockFs({
'/var/run/secrets/kubernetes.io/serviceaccount/ca.crt': '',
});
Config.SERVICEACCOUNT_CA_PATH = mockCertDir.resolve('ca.crt');
worker.use(
rest.get('https://10.10.10.10/api/v1/pods', (req, res, ctx) =>
res(
+1 -2
View File
@@ -63,17 +63,16 @@
"js-yaml": "^4.0.0",
"json5": "^2.1.3",
"mime-types": "^2.1.27",
"mock-fs": "^5.2.0",
"p-limit": "^3.1.0",
"recursive-readdir": "^2.2.2",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/fs-extra": "^9.0.5",
"@types/js-yaml": "^4.0.0",
"@types/mime-types": "^2.1.0",
"@types/mock-fs": "^4.13.0",
"@types/recursive-readdir": "^2.2.0",
"@types/supertest": "^2.0.8",
"aws-sdk-client-mock": "^2.0.0",
@@ -17,9 +17,8 @@
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { createMockDirectory } from '@backstage/backend-test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import os from 'os';
import path, { resolve as resolvePath } from 'path';
import { ParsedLocationAnnotation } from '../../helpers';
import {
@@ -88,14 +87,12 @@ const mkdocsYmlWithEnvTag = fs.readFileSync(
const mockLogger = getVoidLogger();
const warn = jest.spyOn(mockLogger, 'warn');
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({}));
describe('helpers', () => {
afterEach(() => {
mockFs.restore();
});
const mockDir = createMockDirectory();
afterEach(mockDir.clear);
describe('getGeneratorKey', () => {
it('should return techdocs as the only generator key', () => {
@@ -188,13 +185,13 @@ describe('helpers', () => {
describe('patchMkdocsYmlPreBuild', () => {
beforeEach(() => {
mockFs({
'/mkdocs.yml': mkdocsYml,
'/mkdocs_default.yml': mkdocsDefaultYml,
'/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl,
'/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri,
'/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
'/mkdocs_with_comments.yml': mkdocsYmlWithComments,
mockDir.setContent({
'mkdocs.yml': mkdocsYml,
'mkdocs_default.yml': mkdocsDefaultYml,
'mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl,
'mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri,
'mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
'mkdocs_with_comments.yml': mkdocsYmlWithComments,
});
});
@@ -205,13 +202,13 @@ describe('helpers', () => {
};
await patchMkdocsYmlPreBuild(
'/mkdocs.yml',
mockDir.resolve('mkdocs.yml'),
mockLogger,
parsedLocationAnnotation,
scmIntegrations,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs.yml');
const updatedMkdocsYml = await fs.readFile(mockDir.resolve('mkdocs.yml'));
expect(updatedMkdocsYml.toString()).toContain(
'repo_url: https://github.com/backstage/backstage',
@@ -225,13 +222,15 @@ describe('helpers', () => {
};
await patchMkdocsYmlPreBuild(
'/mkdocs_with_extensions.yml',
mockDir.resolve('mkdocs_with_extensions.yml'),
mockLogger,
parsedLocationAnnotation,
scmIntegrations,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_extensions.yml');
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_extensions.yml'),
);
expect(updatedMkdocsYml.toString()).toContain(
'repo_url: https://github.com/backstage/backstage',
@@ -248,13 +247,15 @@ describe('helpers', () => {
};
await patchMkdocsYmlPreBuild(
'/mkdocs_with_repo_url.yml',
mockDir.resolve('mkdocs_with_repo_url.yml'),
mockLogger,
parsedLocationAnnotation,
scmIntegrations,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml');
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_repo_url.yml'),
);
expect(updatedMkdocsYml.toString()).toContain(
'repo_url: https://github.com/backstage/backstage',
@@ -271,13 +272,15 @@ describe('helpers', () => {
};
await patchMkdocsYmlPreBuild(
'/mkdocs_with_edit_uri.yml',
mockDir.resolve('mkdocs_with_edit_uri.yml'),
mockLogger,
parsedLocationAnnotation,
scmIntegrations,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_edit_uri.yml');
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_edit_uri.yml'),
);
expect(updatedMkdocsYml.toString()).toContain(
'edit_uri: https://github.com/backstage/backstage/edit/main/docs',
@@ -294,13 +297,15 @@ describe('helpers', () => {
};
await patchMkdocsYmlPreBuild(
'/mkdocs_with_comments.yml',
mockDir.resolve('mkdocs_with_comments.yml'),
mockLogger,
parsedLocationAnnotation,
scmIntegrations,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_comments.yml');
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_comments.yml'),
);
expect(updatedMkdocsYml.toString()).toContain(
'# This is a comment that is removed after editing',
@@ -312,20 +317,20 @@ describe('helpers', () => {
describe('patchMkdocsYmlWithPlugins', () => {
beforeEach(() => {
mockFs({
'/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
'/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
'/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
mockDir.setContent({
'mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
'mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
'mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
});
});
it('should not add additional plugins if techdocs exists already in mkdocs file', async () => {
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_techdocs_plugin.yml',
mockDir.resolve('mkdocs_with_techdocs_plugin.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
'/mkdocs_with_techdocs_plugin.yml',
mockDir.resolve('mkdocs_with_techdocs_plugin.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
plugins: string[];
@@ -335,11 +340,13 @@ describe('helpers', () => {
});
it("should add the needed plugin if it doesn't exist in mkdocs file", async () => {
await patchMkdocsYmlWithPlugins(
'/mkdocs_without_plugins.yml',
mockDir.resolve('mkdocs_without_plugins.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml');
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_without_plugins.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
plugins: string[];
};
@@ -348,11 +355,11 @@ describe('helpers', () => {
});
it('should not override existing plugins', async () => {
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_additional_plugins.yml',
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
'/mkdocs_with_additional_plugins.yml',
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
plugins: string[];
@@ -364,13 +371,13 @@ describe('helpers', () => {
});
it('should add all provided default plugins', async () => {
await patchMkdocsYmlWithPlugins(
'/mkdocs_with_additional_plugins.yml',
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
mockLogger,
['techdocs-core', 'custom-plugin'],
);
const updatedMkdocsYml = await fs.readFile(
'/mkdocs_with_additional_plugins.yml',
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
plugins: string[];
@@ -386,45 +393,45 @@ describe('helpers', () => {
warn.mockClear();
});
it('should have no effect if docs/index.md exists', async () => {
mockFs({
'/docs/index.md': 'index.md content',
'/docs/README.md': 'docs/README.md content',
mockDir.setContent({
'docs/index.md': 'index.md content',
'docs/README.md': 'docs/README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
'index.md content',
);
await expect(
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
).resolves.toEqual('index.md content');
expect(warn).not.toHaveBeenCalledWith();
});
it("should use docs/README.md if docs/index.md doesn't exists", async () => {
mockFs({
'/docs/README.md': 'docs/README.md content',
'/README.md': 'main README.md content',
mockDir.setContent({
'docs/README.md': 'docs/README.md content',
'README.md': 'main README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
'docs/README.md content',
);
await expect(
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
).resolves.toEqual('docs/README.md content');
expect(warn.mock.calls).toEqual([
[`${path.normalize('docs/index.md')} not found.`],
]);
});
it('should use README.md if neither docs/index.md or docs/README.md exist', async () => {
mockFs({
'/README.md': 'main README.md content',
mockDir.setContent({
'README.md': 'main README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
'main README.md content',
);
await expect(
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
).resolves.toEqual('main README.md content');
expect(warn.mock.calls).toEqual([
[`${path.normalize('docs/index.md')} not found.`],
[`${path.normalize('docs/README.md')} not found.`],
@@ -433,11 +440,13 @@ describe('helpers', () => {
});
it('should not use any file as index.md if no one matches the requirements', async () => {
mockFs({});
mockDir.setContent({});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
await expect(fs.readFile('/docs/index.md', 'utf-8')).rejects.toThrow();
await expect(
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
).rejects.toThrow();
const paths = [
path.normalize('docs/index.md'),
path.normalize('docs/README.md'),
@@ -449,7 +458,7 @@ describe('helpers', () => {
...paths.map(p => [`${p} not found.`]),
[
`Could not find any techdocs' index file. Please make sure at least one of ${paths
.map(p => path.sep + p)
.map(p => mockDir.resolve(p))
.join(' ')} exists.`,
],
]);
@@ -463,13 +472,11 @@ describe('helpers', () => {
};
beforeEach(() => {
mockFs({
[rootDir]: mockFiles,
});
mockDir.setContent(mockFiles);
});
it('should create the file if it does not exist', async () => {
const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json');
const filePath = mockDir.resolve('wrong_techdocs_metadata.json');
await createOrUpdateMetadata(filePath, mockLogger);
// Check if the file exists
@@ -479,7 +486,7 @@ describe('helpers', () => {
});
it('should throw error when the JSON is invalid', async () => {
const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json');
const filePath = mockDir.resolve('invalid_techdocs_metadata.json');
await expect(
createOrUpdateMetadata(filePath, mockLogger),
@@ -487,7 +494,7 @@ describe('helpers', () => {
});
it('should add build timestamp to the metadata json', async () => {
const filePath = path.join(rootDir, 'techdocs_metadata.json');
const filePath = mockDir.resolve('techdocs_metadata.json');
await createOrUpdateMetadata(filePath, mockLogger);
@@ -496,7 +503,7 @@ describe('helpers', () => {
});
it('should add list of files to the metadata json', async () => {
const filePath = path.join(rootDir, 'techdocs_metadata.json');
const filePath = mockDir.resolve('techdocs_metadata.json');
await createOrUpdateMetadata(filePath, mockLogger);
@@ -508,16 +515,14 @@ describe('helpers', () => {
describe('storeEtagMetadata', () => {
beforeEach(() => {
mockFs({
[rootDir]: {
'invalid_techdocs_metadata.json': 'dsds',
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
},
mockDir.setContent({
'invalid_techdocs_metadata.json': 'dsds',
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
});
});
it('should throw error when the JSON is invalid', async () => {
const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json');
const filePath = mockDir.resolve('invalid_techdocs_metadata.json');
await expect(storeEtagMetadata(filePath, 'etag123abc')).rejects.toThrow(
'Unexpected token',
@@ -525,7 +530,7 @@ describe('helpers', () => {
});
it('should add etag to the metadata json', async () => {
const filePath = path.join(rootDir, 'techdocs_metadata.json');
const filePath = mockDir.resolve('techdocs_metadata.json');
await storeEtagMetadata(filePath, 'etag123abc');
@@ -535,34 +540,31 @@ describe('helpers', () => {
});
describe('getMkdocsYml', () => {
const inputDir = resolvePath(__filename, '../__fixtures__/');
const siteOptions = {
name: mockEntity.metadata.title,
};
it('returns expected contents when .yml file is present', async () => {
const key = path.join(inputDir, 'mkdocs.yml');
mockFs({ [key]: mkdocsYml });
mockDir.setContent({ 'mkdocs.yml': mkdocsYml });
const {
path: mkdocsPath,
content,
configIsTemporary,
} = await getMkdocsYml(inputDir, siteOptions);
} = await getMkdocsYml(mockDir.path, siteOptions);
expect(mkdocsPath).toBe(key);
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml'));
expect(content).toBe(mkdocsYml.toString());
expect(configIsTemporary).toBe(false);
});
it('returns expected contents when .yaml file is present', async () => {
const key = path.join(inputDir, 'mkdocs.yaml');
mockFs({ [key]: mkdocsYml });
mockDir.setContent({ 'mkdocs.yaml': mkdocsYml });
const {
path: mkdocsPath,
content,
configIsTemporary,
} = await getMkdocsYml(inputDir, siteOptions);
expect(mkdocsPath).toBe(key);
} = await getMkdocsYml(mockDir.path, siteOptions);
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yaml'));
expect(content).toBe(mkdocsYml.toString());
expect(configIsTemporary).toBe(false);
});
@@ -571,17 +573,16 @@ describe('helpers', () => {
const defaultSiteOptions = {
name: 'Default Test site name',
};
const key = path.join(inputDir, 'mkdocs.yml');
const mockPathExists = jest.spyOn(fs, 'pathExists');
mockPathExists.mockImplementation(() => Promise.resolve(false));
mockFs({ [key]: mkdocsDefaultYml });
mockDir.setContent({ 'mkdocs.yml': mkdocsDefaultYml });
const {
path: mkdocsPath,
content,
configIsTemporary,
} = await getMkdocsYml(inputDir, defaultSiteOptions);
} = await getMkdocsYml(mockDir.path, defaultSiteOptions);
expect(mkdocsPath).toBe(key);
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml'));
expect(content.split(/[\r\n]+/g)).toEqual(
mkdocsDefaultYml.toString().split(/[\r\n]+/g),
);
@@ -35,16 +35,17 @@ import {
import { mockClient, AwsClientStub } from 'aws-sdk-client-mock';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import path from 'path';
import fs from 'fs-extra';
import { AwsS3Publish } from './awsS3';
import { storageRootDir } from '../../testUtils/StorageFilesMock';
import { Readable } from 'stream';
import { createMockDirectory } from '@backstage/backend-test-utils';
const env = process.env;
let s3Mock: AwsClientStub<S3Client>;
const mockDir = createMockDirectory();
function getMockCredentialProvider(): Promise<AwsCredentialProvider> {
return Promise.resolve({
sdkCredentialProvider: async () => {
@@ -66,7 +67,7 @@ const getEntityRootDir = (entity: Entity) => {
metadata: { namespace, name },
} = entity;
return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name);
return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name);
};
class ErrorReadable extends Readable {
@@ -182,27 +183,23 @@ describe('AwsS3Publish', () => {
getMockCredentialProvider(),
);
mockFs({
mockDir.setContent({
[directory]: files,
});
const { StorageFilesMock } = require('../../testUtils/StorageFilesMock');
const storage = new StorageFilesMock();
storage.emptyFiles();
s3Mock = mockClient(S3Client);
s3Mock.on(HeadObjectCommand).callsFake(input => {
if (!storage.fileExists(input.Key)) {
if (!fs.pathExistsSync(mockDir.resolve(input.Key))) {
throw new Error('File does not exist');
}
return {};
});
s3Mock.on(GetObjectCommand).callsFake(input => {
if (storage.fileExists(input.Key)) {
if (fs.pathExistsSync(mockDir.resolve(input.Key))) {
return {
Body: Readable.from(storage.readFile(input.Key)),
Body: Readable.from(fs.readFileSync(mockDir.resolve(input.Key))),
};
}
@@ -237,12 +234,11 @@ describe('AwsS3Publish', () => {
s3Mock.on(UploadPartCommand).rejects();
s3Mock.on(PutObjectCommand).callsFake(input => {
storage.writeFile(input.Key, input.Body);
mockDir.addContent({ [input.Key]: input.Body });
});
});
afterEach(() => {
mockFs.restore();
process.env = env;
});
@@ -378,8 +374,7 @@ describe('AwsS3Publish', () => {
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
storageRootDir,
const wrongPathToGeneratedDirectory = mockDir.resolve(
'wrong',
'path',
'to',
@@ -393,11 +388,9 @@ describe('AwsS3Publish', () => {
directory: wrongPathToGeneratedDirectory,
});
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(
'Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT, no such file or directory',
),
});
await expect(fails).rejects.toThrow(
`Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`,
);
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(wrongPathToGeneratedDirectory),
@@ -19,7 +19,6 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import path from 'path';
import fs from 'fs-extra';
import { AzureBlobStoragePublish } from './azureBlobStorage';
@@ -28,10 +27,9 @@ import {
BlobUploadCommonResponse,
ContainerGetPropertiesResponse,
} from '@azure/storage-blob';
import {
storageRootDir,
StorageFilesMock,
} from '../../testUtils/StorageFilesMock';
import { createMockDirectory } from '@backstage/backend-test-utils';
const mockDir = createMockDirectory();
jest.mock('@azure/identity', () => ({
__esModule: true,
@@ -40,13 +38,12 @@ jest.mock('@azure/identity', () => ({
jest.mock('@azure/storage-blob', () => {
class BlockBlobClient {
constructor(
private readonly blobName: string,
private readonly storage: StorageFilesMock,
) {}
constructor(private readonly blobName: string) {}
uploadFile(source: string): Promise<BlobUploadCommonResponse> {
this.storage.writeFile(this.blobName, source);
mockDir.addContent({
[this.blobName]: fs.readFileSync(source, 'utf8'),
});
return Promise.resolve({
_response: {
request: {
@@ -59,14 +56,14 @@ jest.mock('@azure/storage-blob', () => {
}
exists() {
return this.storage.fileExists(this.blobName);
return fs.pathExistsSync(mockDir.resolve(this.blobName));
}
download() {
const emitter = new EventEmitter();
setTimeout(() => {
if (this.storage.fileExists(this.blobName)) {
emitter.emit('data', this.storage.readFile(this.blobName));
if (fs.pathExistsSync(mockDir.resolve(this.blobName))) {
emitter.emit('data', fs.readFileSync(mockDir.resolve(this.blobName)));
emitter.emit('end');
} else {
emitter.emit(
@@ -126,10 +123,7 @@ jest.mock('@azure/storage-blob', () => {
}
class ContainerClient {
constructor(
private readonly containerName: string,
protected readonly storage: StorageFilesMock,
) {}
constructor(private readonly containerName: string) {}
getProperties(): Promise<ContainerGetPropertiesResponse> {
return Promise.resolve({
@@ -145,7 +139,7 @@ jest.mock('@azure/storage-blob', () => {
}
getBlockBlobClient(blobName: string) {
return new BlockBlobClient(blobName, this.storage);
return new BlockBlobClient(blobName);
}
listBlobsFlat() {
@@ -180,31 +174,24 @@ jest.mock('@azure/storage-blob', () => {
class ContainerClientFailUpload extends ContainerClient {
getBlockBlobClient(blobName: string) {
return new BlockBlobClientFailUpload(blobName, this.storage);
return new BlockBlobClientFailUpload(blobName);
}
}
class BlobServiceClient {
storage = new StorageFilesMock();
constructor(
public readonly url: string,
private readonly credential?: StorageSharedKeyCredential,
) {
this.storage.emptyFiles();
}
) {}
getContainerClient(containerName: string) {
if (containerName === 'bad_container') {
return new ContainerClientFailGetProperties(
containerName,
this.storage,
);
return new ContainerClientFailGetProperties(containerName);
}
if (this.credential?.accountName === 'bad_account_credentials') {
return new ContainerClientFailUpload(containerName, this.storage);
return new ContainerClientFailUpload(containerName);
}
return new ContainerClient(containerName, this.storage);
return new ContainerClient(containerName);
}
}
@@ -231,7 +218,7 @@ const getEntityRootDir = (entity: Entity) => {
metadata: { namespace, name },
} = entity;
return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name);
return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name);
};
const logger = getVoidLogger();
@@ -314,15 +301,11 @@ describe('AzureBlobStoragePublish', () => {
};
beforeEach(async () => {
mockFs({
mockDir.setContent({
[directory]: files,
});
});
afterEach(() => {
mockFs.restore();
});
describe('getReadiness', () => {
it('should validate correct config', async () => {
const publisher = createPublisherFromConfig();
@@ -374,8 +357,7 @@ describe('AzureBlobStoragePublish', () => {
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
storageRootDir,
const wrongPathToGeneratedDirectory = mockDir.resolve(
'wrong',
'path',
'to',
@@ -391,11 +373,9 @@ describe('AzureBlobStoragePublish', () => {
directory: wrongPathToGeneratedDirectory,
});
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(
'Unable to upload file(s) to Azure. Error: Failed to read template directory: ENOENT, no such file or directory',
),
});
await expect(fails).rejects.toThrow(
`Unable to upload file(s) to Azure. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`,
);
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(wrongPathToGeneratedDirectory),
@@ -19,26 +19,21 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import path from 'path';
import fs from 'fs-extra';
import { Readable } from 'stream';
import { GoogleGCSPublish } from './googleStorage';
import {
storageRootDir,
StorageFilesMock,
} from '../../testUtils/StorageFilesMock';
import { createMockDirectory } from '@backstage/backend-test-utils';
const mockDir = createMockDirectory();
jest.mock('@google-cloud/storage', () => {
class GCSFile {
constructor(
private readonly filePath: string,
private readonly storage: StorageFilesMock,
) {}
constructor(private readonly filePath: string) {}
exists() {
return new Promise(async (resolve, reject) => {
if (this.storage.fileExists(this.filePath)) {
if (fs.pathExistsSync(mockDir.resolve(this.filePath))) {
resolve([true]);
} else {
reject();
@@ -51,11 +46,14 @@ jest.mock('@google-cloud/storage', () => {
readable._read = () => {};
process.nextTick(() => {
if (this.storage.fileExists(this.filePath)) {
if (fs.pathExistsSync(mockDir.resolve(this.filePath))) {
if (readable.eventNames().includes('pipe')) {
readable.emit('pipe');
}
readable.emit('data', this.storage.readFile(this.filePath));
readable.emit(
'data',
fs.readFileSync(mockDir.resolve(this.filePath)),
);
readable.emit('end');
} else {
readable.emit(
@@ -74,10 +72,7 @@ jest.mock('@google-cloud/storage', () => {
}
class Bucket {
constructor(
private readonly bucketName: string,
private readonly storage: StorageFilesMock,
) {}
constructor(private readonly bucketName: string) {}
async getMetadata() {
if (this.bucketName === 'bad_bucket_name') {
@@ -88,7 +83,9 @@ jest.mock('@google-cloud/storage', () => {
upload(source: string, { destination }: { destination: string }) {
return new Promise(async resolve => {
this.storage.writeFile(destination, source);
mockDir.addContent({
[destination]: fs.readFileSync(source, 'utf8'),
});
resolve(null);
});
}
@@ -97,7 +94,7 @@ jest.mock('@google-cloud/storage', () => {
if (this.bucketName === 'delete_stale_files_error') {
throw Error('Message');
}
return new GCSFile(destinationFilePath, this.storage);
return new GCSFile(destinationFilePath);
}
getFilesStream() {
@@ -119,14 +116,8 @@ jest.mock('@google-cloud/storage', () => {
}
class Storage {
storage = new StorageFilesMock();
constructor() {
this.storage.emptyFiles();
}
bucket(bucketName: string) {
return new Bucket(bucketName, this.storage);
return new Bucket(bucketName);
}
}
@@ -142,7 +133,7 @@ const getEntityRootDir = (entity: Entity) => {
metadata: { namespace, name },
} = entity;
return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name);
return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name);
};
const logger = getVoidLogger();
@@ -220,15 +211,11 @@ describe('GoogleGCSPublish', () => {
};
beforeEach(() => {
mockFs({
mockDir.setContent({
[directory]: files,
});
});
afterEach(() => {
mockFs.restore();
});
describe('getReadiness', () => {
it('should validate correct config', async () => {
const publisher = createPublisherFromConfig();
@@ -300,8 +287,7 @@ describe('GoogleGCSPublish', () => {
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
storageRootDir,
const wrongPathToGeneratedDirectory = mockDir.resolve(
'wrong',
'path',
'to',
@@ -315,13 +301,9 @@ describe('GoogleGCSPublish', () => {
directory: wrongPathToGeneratedDirectory,
});
// Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
// Issue reported https://github.com/tschaub/mock-fs/issues/118
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(
`Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`,
),
});
await expect(fails).rejects.toThrow(
`Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`,
);
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(wrongPathToGeneratedDirectory),
@@ -13,9 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mockFs from 'mock-fs';
import * as os from 'os';
import * as path from 'path';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import {
getStaleFiles,
@@ -27,6 +24,7 @@ import {
lowerCaseEntityTripletInStoragePath,
normalizeExternalStorageRootPath,
} from './helpers';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('getHeadersForFileExtension', () => {
const correctMapOfExtensions = [
@@ -57,30 +55,24 @@ describe('getHeadersForFileExtension', () => {
});
describe('getFileTreeRecursively', () => {
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const mockDir = createMockDirectory();
beforeEach(() => {
mockFs({
[root]: {
file1: '',
subDirA: {
file2: '',
emptyDir1: mockFs.directory(),
},
emptyDir2: mockFs.directory(),
mockDir.setContent({
file1: '',
subDirA: {
file2: '',
emptyDir1: {},
},
emptyDir2: {},
});
});
afterEach(() => {
mockFs.restore();
});
it('returns complete file tree of a path', async () => {
const fileList = await getFileTreeRecursively(root);
const fileList = await getFileTreeRecursively(mockDir.path);
expect(fileList.length).toBe(2);
expect(fileList).toContain(path.resolve(root, 'file1'));
expect(fileList).toContain(path.resolve(root, 'subDirA/file2'));
expect(fileList).toContain(mockDir.resolve('file1'));
expect(fileList).toContain(mockDir.resolve('subDirA/file2'));
});
});
@@ -16,15 +16,15 @@
import {
getVoidLogger,
PluginEndpointDiscovery,
resolvePackagePath,
} from '@backstage/backend-common';
import { overridePackagePathResolution } from '@backstage/backend-common/testUtils';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import * as os from 'os';
import { LocalPublish } from './local';
import path from 'path';
import { createMockDirectory } from '@backstage/backend-test-utils';
const createMockEntity = (annotations = {}, lowerCase = false) => {
return {
@@ -44,30 +44,28 @@ const testDiscovery: jest.Mocked<PluginEndpointDiscovery> = {
getExternalBaseUrl: jest.fn(),
};
const mockPublishDir = createMockDirectory();
overridePackagePathResolution({
packageName: '@backstage/plugin-techdocs-backend',
paths: {
'static/docs': mockPublishDir.path,
},
});
const logger = getVoidLogger();
const tmpDir =
os.platform() === 'win32' ? 'C:\\tmp\\generatedDir' : '/tmp/generatedDir';
const resolvedDir = resolvePackagePath(
'@backstage/plugin-techdocs-backend',
'static/docs',
);
describe('local publisher', () => {
const mockDir = createMockDirectory();
describe('publish', () => {
beforeEach(() => {
mockFs({
[tmpDir]: {
'index.html': '',
},
mockPublishDir.clear();
mockDir.setContent({
'index.html': '',
});
});
afterEach(() => {
mockFs.restore();
});
it('should publish generated documentation dir', async () => {
const mockConfig = new ConfigReader({});
@@ -79,7 +77,7 @@ describe('local publisher', () => {
const mockEntity = createMockEntity();
const lowerMockEntity = createMockEntity(undefined, true);
await publisher.publish({ entity: mockEntity, directory: tmpDir });
await publisher.publish({ entity: mockEntity, directory: mockDir.path });
expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true);
@@ -102,12 +100,14 @@ describe('local publisher', () => {
const mockEntity = createMockEntity();
const lowerMockEntity = createMockEntity(undefined, true);
await publisher.publish({ entity: mockEntity, directory: tmpDir });
await publisher.publish({ entity: mockEntity, directory: mockDir.path });
expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true);
// Lower/upper should be treated differently.
expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(false);
expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(
os.platform() === 'darwin', // MacOS is case-insensitive
);
});
it('should throw with unsafe triplet', async () => {
@@ -126,7 +126,7 @@ describe('local publisher', () => {
};
await expect(() =>
publisher.publish({ entity: mockEntity, directory: tmpDir }),
publisher.publish({ entity: mockEntity, directory: mockDir.path }),
).rejects.toThrow('Unable to publish TechDocs site');
});
@@ -149,7 +149,7 @@ describe('local publisher', () => {
};
await expect(() =>
publisher.publish({ entity: mockEntity, directory: tmpDir }),
publisher.publish({ entity: mockEntity, directory: mockDir.path }),
).rejects.toThrow('Unable to publish TechDocs site');
});
});
@@ -165,25 +165,19 @@ describe('local publisher', () => {
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs({
[resolvedDir]: {
'unsafe.html': '<html></html>',
'unsafe.svg': '<svg></svg>',
default: {
testkind: {
testname: {
'index.html': 'found it',
},
mockPublishDir.setContent({
'unsafe.html': '<html></html>',
'unsafe.svg': '<svg></svg>',
default: {
testkind: {
testname: {
'index.html': 'found it',
},
},
},
});
});
afterEach(() => {
mockFs.restore();
});
it('should pass text/plain content-type for unsafe types', async () => {
const htmlResponse = await request(app).get(`/unsafe.html`);
expect(htmlResponse.text).toEqual('<html></html>');
@@ -228,7 +222,7 @@ describe('local publisher', () => {
const response = await request(app).get(
'/default/TestKind/TestName/index.html',
);
expect(response.status).toBe(404);
expect(response.status).toBe(os.platform() === 'darwin' ? 200 : 404);
});
it('should work with a configured directory', async () => {
@@ -236,15 +230,13 @@ describe('local publisher', () => {
techdocs: {
publisher: {
local: {
publishDirectory: tmpDir,
publishDirectory: mockDir.path,
},
},
},
});
mockFs({
[tmpDir]: {
'index.html': 'found it',
},
mockDir.setContent({
'index.html': 'found it',
});
const legacyPublisher = LocalPublish.fromConfig(
customConfig,
@@ -23,13 +23,14 @@ import {
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import fs from 'fs-extra';
import path from 'path';
import { OpenStackSwiftPublish } from './openStackSwift';
import { PublisherBase, TechDocsMetadata } from './types';
import { storageRootDir } from '../../testUtils/StorageFilesMock';
import { Stream, Readable } from 'stream';
import { createMockDirectory } from '@backstage/backend-test-utils';
const mockDir = createMockDirectory();
jest.mock('@trendyol-js/openstack-swift-sdk', () => {
const {
@@ -45,7 +46,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => {
const checkFileExists = async (Key: string): Promise<boolean> => {
// Key will always have / as file separator irrespective of OS since cloud providers expects /.
// Normalize Key to OS specific path before checking if file exists.
const filePath = path.join(storageRootDir, Key);
const filePath = mockDir.resolve(Key);
try {
await fs.access(filePath, fs.constants.F_OK);
@@ -96,7 +97,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => {
stream: Readable,
) {
try {
const filePath = path.join(storageRootDir, destination);
const filePath = mockDir.resolve(destination);
const fileBuffer = await streamToBuffer(stream);
await fs.writeFile(filePath, fileBuffer);
@@ -114,7 +115,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => {
}
async download(_containerName: string, file: string) {
const filePath = path.join(storageRootDir, file);
const filePath = mockDir.resolve(file);
const fileExists = await checkFileExists(file);
if (!fileExists) {
return new NotFound();
@@ -151,7 +152,7 @@ const getEntityRootDir = (entity: Entity) => {
metadata: { namespace, name },
} = entity;
return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name);
return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name);
};
const getPosixEntityRootDir = (entity: Entity) => {
@@ -193,11 +194,11 @@ beforeEach(() => {
publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger);
});
afterEach(() => {
mockFs.restore();
});
describe('OpenStackSwiftPublish', () => {
afterEach(() => {
mockDir.clear();
});
describe('getReadiness', () => {
it('should validate correct config', async () => {
expect(await publisher.getReadiness()).toEqual({
@@ -239,7 +240,7 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
mockDir.setContent({
[entityRootDir]: {
'index.html': '',
'404.html': '',
@@ -254,12 +255,9 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
expect(
await publisher.publish({
entity,
directory: entityRootDir,
}),
).toMatchObject({
await expect(
publisher.publish({ entity, directory: entityRootDir }),
).resolves.toMatchObject({
objects: expect.arrayContaining([
'test-namespace/TestKind/test-component-name/404.html',
`test-namespace/TestKind/test-component-name/index.html`,
@@ -269,8 +267,7 @@ describe('OpenStackSwiftPublish', () => {
});
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
storageRootDir,
const wrongPathToGeneratedDirectory = mockDir.resolve(
'wrong',
'path',
'to',
@@ -290,13 +287,9 @@ describe('OpenStackSwiftPublish', () => {
directory: wrongPathToGeneratedDirectory,
});
// Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
// Issue reported https://github.com/tschaub/mock-fs/issues/118
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(
`Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`,
),
});
await expect(fails).rejects.toThrow(
`Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`,
);
await expect(fails).rejects.toMatchObject({
message: expect.stringContaining(wrongPathToGeneratedDirectory),
});
@@ -308,7 +301,7 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
mockDir.setContent({
[entityRootDir]: {
'index.html': 'file-content',
},
@@ -330,7 +323,7 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
mockDir.setContent({
[entityRootDir]: {
'techdocs_metadata.json':
'{"site_name": "backstage", "site_description": "site_content", "etag": "etag", "build_timestamp": 612741599}',
@@ -353,7 +346,7 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
mockDir.setContent({
[entityRootDir]: {
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag', 'build_timestamp': 612741599}`,
},
@@ -393,7 +386,7 @@ describe('OpenStackSwiftPublish', () => {
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs({
mockDir.setContent({
[entityRootDir]: {
html: {
'unsafe.html': '<html></html>',
@@ -1,61 +0,0 @@
/*
* Copyright 2020 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 os from 'os';
import path from 'path';
import fs from 'fs-extra';
import { IStorageFilesMock } from './types';
export const storageRootDir: string =
os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const encoding = 'utf8';
export class StorageFilesMock implements IStorageFilesMock {
static rootDir = storageRootDir;
private files: Record<string, string>;
constructor() {
this.files = {};
}
public emptyFiles(): void {
this.files = {};
}
public fileExists(targetPath: string): boolean {
const filePath = path.join(storageRootDir, targetPath);
const posixPath = filePath.split(path.posix.sep).join(path.sep);
return this.files[posixPath] !== undefined;
}
public readFile(targetPath: string): Buffer {
const filePath = path.join(storageRootDir, targetPath);
return Buffer.from(this.files[filePath] ?? '', encoding);
}
public writeFile(targetPath: string, sourcePath: string): void;
public writeFile(targetPath: string, sourceBuffer: Buffer): void;
public writeFile(targetPath: string, source: string | Buffer): void {
const filePath = path.join(storageRootDir, targetPath);
if (typeof source === 'string') {
this.files[filePath] = fs.readFileSync(source).toString(encoding);
} else {
this.files[filePath] = source.toString(encoding);
}
}
}
@@ -1,24 +0,0 @@
/*
* Copyright 2020 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.
*/
export interface IStorageFilesMock {
emptyFiles(): void;
fileExists(targetPath: string): boolean;
readFile(targetPath: string): Buffer;
writeFile(targetPath: string, sourcePath: string): void;
writeFile(targetPath: string, sourceBuffer: Buffer): void;
writeFile(targetPath: string, source: string | Buffer): void;
}
+1 -4
View File
@@ -4833,7 +4833,6 @@ __metadata:
knex: ^2.0.0
lodash: ^4.17.21
luxon: ^3.0.0
mock-fs: ^5.2.0
msw: ^1.0.0
node-fetch: ^2.6.7
supertest: ^6.1.3
@@ -7600,7 +7599,6 @@ __metadata:
http-proxy-middleware: ^2.0.6
lodash: ^4.17.21
luxon: ^3.0.0
mock-fs: ^5.2.0
morgan: ^1.10.0
msw: ^1.0.0
node-fetch: ^2.6.7
@@ -9688,6 +9686,7 @@ __metadata:
"@azure/identity": ^3.2.1
"@azure/storage-blob": ^12.5.0
"@backstage/backend-common": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
@@ -9701,7 +9700,6 @@ __metadata:
"@types/fs-extra": ^9.0.5
"@types/js-yaml": ^4.0.0
"@types/mime-types": ^2.1.0
"@types/mock-fs": ^4.13.0
"@types/recursive-readdir": ^2.2.0
"@types/supertest": ^2.0.8
aws-sdk-client-mock: ^2.0.0
@@ -9712,7 +9710,6 @@ __metadata:
js-yaml: ^4.0.0
json5: ^2.1.3
mime-types: ^2.1.27
mock-fs: ^5.2.0
p-limit: ^3.1.0
recursive-readdir: ^2.2.2
supertest: ^6.1.3