frontend-test-utils: copy over all mock APIs
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -32,13 +32,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-app-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-app": "workspace:^",
|
||||
"@backstage/plugin-app-react": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/plugin-permission-react": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"i18next": "^22.4.15",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -47,6 +53,7 @@
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"msw": "^2.0.0",
|
||||
"react": "^18.0.2",
|
||||
"react-dom": "^18.0.2",
|
||||
"react-router-dom": "^6.30.2"
|
||||
|
||||
@@ -12,7 +12,19 @@ import { ApiHolder } from '@backstage/frontend-plugin-api';
|
||||
import { ApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ConfigApi as ConfigApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import crossFetch from 'cross-fetch';
|
||||
import { DiscoveryApi } from '@backstage/frontend-plugin-api';
|
||||
import { DiscoveryApi as DiscoveryApi_2 } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi as ErrorApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorApiError } from '@backstage/core-plugin-api';
|
||||
import { ErrorApiErrorContext } from '@backstage/core-plugin-api';
|
||||
import { EvaluatePermissionRequest } from '@backstage/plugin-permission-common';
|
||||
import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api';
|
||||
@@ -20,24 +32,28 @@ import { FeatureFlag } from '@backstage/frontend-plugin-api';
|
||||
import { FeatureFlagsApi } from '@backstage/frontend-plugin-api';
|
||||
import { FeatureFlagsSaveOptions } from '@backstage/frontend-plugin-api';
|
||||
import { FeatureFlagState } from '@backstage/frontend-plugin-api';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { FetchApi as FetchApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { IdentityApi } from '@backstage/frontend-plugin-api';
|
||||
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { mockApis as mockApis_2 } from '@backstage/test-utils';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { MockErrorApi } from '@backstage/test-utils';
|
||||
import { MockErrorApiOptions } from '@backstage/test-utils';
|
||||
import { MockFetchApi } from '@backstage/test-utils';
|
||||
import { MockFetchApiOptions } from '@backstage/test-utils';
|
||||
import { MockPermissionApi } from '@backstage/test-utils';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { MockStorageBucket } from '@backstage/test-utils';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PermissionApi } from '@backstage/plugin-permission-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { StorageApi as StorageApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
|
||||
import { testingLibraryDomTypesQueries } from '@testing-library/dom/types/queries';
|
||||
import { TranslationApi } from '@backstage/core-plugin-api/alpha';
|
||||
import { TranslationApi as TranslationApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { TranslationSnapshot } from '@backstage/core-plugin-api/alpha';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @public
|
||||
@@ -62,7 +78,11 @@ export function createExtensionTester<
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>>;
|
||||
|
||||
export { ErrorWithContext };
|
||||
// @public
|
||||
export type ErrorWithContext = {
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
@@ -160,6 +180,97 @@ export namespace mockApis {
|
||||
alert$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function analytics(): MockAnalyticsApi &
|
||||
MockWithApiFactory<AnalyticsApi>;
|
||||
export namespace analytics {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
captureEvent: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
captureEvent: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function config(options?: {
|
||||
data?: JsonObject;
|
||||
}): MockConfigApi & MockWithApiFactory<ConfigApi_2>;
|
||||
export namespace config {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
has: jest.Mock<any, any, any>;
|
||||
keys: jest.Mock<any, any, any>;
|
||||
get: jest.Mock<any, any, any>;
|
||||
getOptional: jest.Mock<any, any, any>;
|
||||
getConfig: jest.Mock<any, any, any>;
|
||||
getOptionalConfig: jest.Mock<any, any, any>;
|
||||
getConfigArray: jest.Mock<any, any, any>;
|
||||
getOptionalConfigArray: jest.Mock<any, any, any>;
|
||||
getNumber: jest.Mock<any, any, any>;
|
||||
getOptionalNumber: jest.Mock<any, any, any>;
|
||||
getBoolean: jest.Mock<any, any, any>;
|
||||
getOptionalBoolean: jest.Mock<any, any, any>;
|
||||
getString: jest.Mock<any, any, any>;
|
||||
getOptionalString: jest.Mock<any, any, any>;
|
||||
getStringArray: jest.Mock<any, any, any>;
|
||||
getOptionalStringArray: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
has: jest.Mock<any, any, any>;
|
||||
keys: jest.Mock<any, any, any>;
|
||||
get: jest.Mock<any, any, any>;
|
||||
getOptional: jest.Mock<any, any, any>;
|
||||
getConfig: jest.Mock<any, any, any>;
|
||||
getOptionalConfig: jest.Mock<any, any, any>;
|
||||
getConfigArray: jest.Mock<any, any, any>;
|
||||
getOptionalConfigArray: jest.Mock<any, any, any>;
|
||||
getNumber: jest.Mock<any, any, any>;
|
||||
getOptionalNumber: jest.Mock<any, any, any>;
|
||||
getBoolean: jest.Mock<any, any, any>;
|
||||
getOptionalBoolean: jest.Mock<any, any, any>;
|
||||
getString: jest.Mock<any, any, any>;
|
||||
getOptionalString: jest.Mock<any, any, any>;
|
||||
getStringArray: jest.Mock<any, any, any>;
|
||||
getOptionalStringArray: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function discovery(options?: {
|
||||
baseUrl?: string;
|
||||
}): DiscoveryApi & MockWithApiFactory<DiscoveryApi>;
|
||||
export namespace discovery {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getBaseUrl: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getBaseUrl: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function error(
|
||||
options?: MockErrorApiOptions,
|
||||
): MockErrorApi & MockWithApiFactory<ErrorApi_2>;
|
||||
export namespace error {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
error$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
error$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function featureFlags(
|
||||
options?: MockFeatureFlagsApiOptions,
|
||||
): MockWithApiFactory<MockFeatureFlagsApi>;
|
||||
@@ -181,27 +292,148 @@ export namespace mockApis {
|
||||
save: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
const // (undocumented)
|
||||
analytics: typeof mockApis_2.analytics;
|
||||
const // (undocumented)
|
||||
config: typeof mockApis_2.config;
|
||||
const // (undocumented)
|
||||
discovery: typeof mockApis_2.discovery;
|
||||
const // (undocumented)
|
||||
identity: typeof mockApis_2.identity;
|
||||
const // (undocumented)
|
||||
permission: typeof mockApis_2.permission;
|
||||
const // (undocumented)
|
||||
storage: typeof mockApis_2.storage;
|
||||
const // (undocumented)
|
||||
translation: typeof mockApis_2.translation;
|
||||
export function fetch(
|
||||
options?: MockFetchApiOptions,
|
||||
): MockFetchApi & MockWithApiFactory<FetchApi_2>;
|
||||
export namespace fetch {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
fetch: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
fetch: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function identity(options?: {
|
||||
userEntityRef?: string;
|
||||
ownershipEntityRefs?: string[];
|
||||
token?: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
picture?: string;
|
||||
}): IdentityApi & MockWithApiFactory<IdentityApi>;
|
||||
export namespace identity {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getBackstageIdentity: jest.Mock<any, any, any>;
|
||||
getCredentials: jest.Mock<any, any, any>;
|
||||
getProfileInfo: jest.Mock<any, any, any>;
|
||||
signOut: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getBackstageIdentity: jest.Mock<any, any, any>;
|
||||
getCredentials: jest.Mock<any, any, any>;
|
||||
getProfileInfo: jest.Mock<any, any, any>;
|
||||
signOut: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function permission(options?: {
|
||||
authorize?:
|
||||
| AuthorizeResult.ALLOW
|
||||
| AuthorizeResult.DENY
|
||||
| ((
|
||||
request: EvaluatePermissionRequest,
|
||||
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY);
|
||||
}): MockPermissionApi & MockWithApiFactory<PermissionApi>;
|
||||
export namespace permission {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
authorize: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
authorize: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function storage(options?: {
|
||||
data?: JsonObject;
|
||||
}): MockStorageApi & MockWithApiFactory<StorageApi_2>;
|
||||
export namespace storage {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
forBucket: jest.Mock<any, any, any>;
|
||||
snapshot: jest.Mock<any, any, any>;
|
||||
set: jest.Mock<any, any, any>;
|
||||
remove: jest.Mock<any, any, any>;
|
||||
observe$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
forBucket: jest.Mock<any, any, any>;
|
||||
snapshot: jest.Mock<any, any, any>;
|
||||
set: jest.Mock<any, any, any>;
|
||||
remove: jest.Mock<any, any, any>;
|
||||
observe$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
export function translation(): MockTranslationApi &
|
||||
MockWithApiFactory<TranslationApi_2>;
|
||||
export namespace translation {
|
||||
const mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getTranslation: jest.Mock<any, any, any>;
|
||||
translation$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getTranslation: jest.Mock<any, any, any>;
|
||||
translation$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
export { MockConfigApi };
|
||||
// @public
|
||||
export class MockConfigApi implements ConfigApi {
|
||||
constructor(data: JsonObject);
|
||||
get<T = JsonValue>(key?: string): T;
|
||||
getBoolean(key: string): boolean;
|
||||
getConfig(key: string): Config;
|
||||
getConfigArray(key: string): Config[];
|
||||
getNumber(key: string): number;
|
||||
getOptional<T = JsonValue>(key?: string): T | undefined;
|
||||
getOptionalBoolean(key: string): boolean | undefined;
|
||||
getOptionalConfig(key: string): Config | undefined;
|
||||
getOptionalConfigArray(key: string): Config[] | undefined;
|
||||
getOptionalNumber(key: string): number | undefined;
|
||||
getOptionalString(key: string): string | undefined;
|
||||
getOptionalStringArray(key: string): string[] | undefined;
|
||||
getString(key: string): string;
|
||||
getStringArray(key: string): string[];
|
||||
has(key: string): boolean;
|
||||
keys(): string[];
|
||||
}
|
||||
|
||||
export { MockErrorApi };
|
||||
// @public
|
||||
export class MockErrorApi implements ErrorApi {
|
||||
constructor(options?: MockErrorApiOptions);
|
||||
// (undocumented)
|
||||
error$(): Observable<{
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
}>;
|
||||
// (undocumented)
|
||||
getErrors(): ErrorWithContext[];
|
||||
// (undocumented)
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext): void;
|
||||
// (undocumented)
|
||||
waitForError(pattern: RegExp, timeoutMs?: number): Promise<ErrorWithContext>;
|
||||
}
|
||||
|
||||
export { MockErrorApiOptions };
|
||||
// @public
|
||||
export type MockErrorApiOptions = {
|
||||
collect?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class MockFeatureFlagsApi implements FeatureFlagsApi {
|
||||
@@ -224,15 +456,85 @@ export interface MockFeatureFlagsApiOptions {
|
||||
initialStates?: Record<string, FeatureFlagState>;
|
||||
}
|
||||
|
||||
export { MockFetchApi };
|
||||
// @public
|
||||
export class MockFetchApi implements FetchApi {
|
||||
constructor(options?: MockFetchApiOptions);
|
||||
get fetch(): typeof crossFetch;
|
||||
}
|
||||
|
||||
export { MockFetchApiOptions };
|
||||
// @public
|
||||
export interface MockFetchApiOptions {
|
||||
baseImplementation?: undefined | 'none' | typeof crossFetch;
|
||||
injectIdentityAuth?:
|
||||
| undefined
|
||||
| {
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
identityApi: Pick<IdentityApi_2, 'getCredentials'>;
|
||||
};
|
||||
resolvePluginProtocol?:
|
||||
| undefined
|
||||
| {
|
||||
discoveryApi: Pick<DiscoveryApi_2, 'getBaseUrl'>;
|
||||
};
|
||||
}
|
||||
|
||||
export { MockPermissionApi };
|
||||
// @public
|
||||
export class MockPermissionApi implements PermissionApi {
|
||||
constructor(
|
||||
requestHandler?: (
|
||||
request: EvaluatePermissionRequest,
|
||||
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY,
|
||||
);
|
||||
// (undocumented)
|
||||
authorize(
|
||||
request: EvaluatePermissionRequest,
|
||||
): Promise<EvaluatePermissionResponse>;
|
||||
}
|
||||
|
||||
export { MockStorageApi };
|
||||
// @public
|
||||
export class MockStorageApi implements StorageApi {
|
||||
// (undocumented)
|
||||
static create(data?: MockStorageBucket): MockStorageApi;
|
||||
// (undocumented)
|
||||
forBucket(name: string): StorageApi;
|
||||
// (undocumented)
|
||||
observe$<T extends JsonValue>(
|
||||
key: string,
|
||||
): Observable<StorageValueSnapshot<T>>;
|
||||
// (undocumented)
|
||||
remove(key: string): Promise<void>;
|
||||
// (undocumented)
|
||||
set<T>(key: string, data: T): Promise<void>;
|
||||
// (undocumented)
|
||||
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
|
||||
}
|
||||
|
||||
export { MockStorageBucket };
|
||||
// @public
|
||||
export type MockStorageBucket = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class MockTranslationApi implements TranslationApi {
|
||||
// (undocumented)
|
||||
static create(): MockTranslationApi;
|
||||
// (undocumented)
|
||||
getTranslation<
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
>(
|
||||
translationRef: TranslationRef<string, TMessages>,
|
||||
): TranslationSnapshot<TMessages>;
|
||||
// (undocumented)
|
||||
translation$<
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
>(): Observable<TranslationSnapshot<TMessages>>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type MockWithApiFactory<TApi> = TApi & {
|
||||
@@ -286,8 +588,13 @@ export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends readonly [
|
||||
infer _Ref,
|
||||
infer _Impl,
|
||||
]
|
||||
? TApi
|
||||
: TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>] | MockWithApiFactory<TApi>
|
||||
: never;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 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 { MockConfigApi } from './MockConfigApi';
|
||||
|
||||
describe('MockConfigApi', () => {
|
||||
it('is able to read some basic config', () => {
|
||||
const mock = new MockConfigApi({
|
||||
app: {
|
||||
title: 'Hello',
|
||||
},
|
||||
x: 1,
|
||||
y: false,
|
||||
z: [{ a: 3 }],
|
||||
});
|
||||
|
||||
expect(mock.getString('app.title')).toEqual('Hello');
|
||||
expect(mock.getNumber('x')).toEqual(1);
|
||||
expect(mock.getBoolean('y')).toEqual(false);
|
||||
expect(mock.getConfigArray('z')[0].getOptionalNumber('a')).toEqual(3);
|
||||
|
||||
expect(() => mock.getString('x')).toThrow(
|
||||
"Invalid type in config for key 'x' in 'mock-config', got number, wanted string",
|
||||
);
|
||||
expect(() => mock.getString('missing')).toThrow(
|
||||
"Missing required config value at 'missing'",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Config, ConfigReader } from '@backstage/config';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* MockConfigApi is a thin wrapper around {@link @backstage/config#ConfigReader}
|
||||
* that can be used to mock configuration using a plain object.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* ```tsx
|
||||
* const mockConfig = new MockConfigApi({
|
||||
* app: { baseUrl: 'https://example.com' },
|
||||
* });
|
||||
*
|
||||
* const rendered = await renderInTestApp(
|
||||
* <TestApiProvider apis={[[configApiRef, mockConfig]]}>
|
||||
* <MyTestedComponent />
|
||||
* </TestApiProvider>,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export class MockConfigApi implements ConfigApi {
|
||||
private readonly config: ConfigReader;
|
||||
|
||||
// NOTE: not extending in order to avoid inheriting the static `.fromConfigs`
|
||||
constructor(data: JsonObject) {
|
||||
this.config = new ConfigReader(data);
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/config#Config.has} */
|
||||
has(key: string): boolean {
|
||||
return this.config.has(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.keys} */
|
||||
keys(): string[] {
|
||||
return this.config.keys();
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.get} */
|
||||
get<T = JsonValue>(key?: string): T {
|
||||
return this.config.get(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptional} */
|
||||
getOptional<T = JsonValue>(key?: string): T | undefined {
|
||||
return this.config.getOptional(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getConfig} */
|
||||
getConfig(key: string): Config {
|
||||
return this.config.getConfig(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalConfig} */
|
||||
getOptionalConfig(key: string): Config | undefined {
|
||||
return this.config.getOptionalConfig(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getConfigArray} */
|
||||
getConfigArray(key: string): Config[] {
|
||||
return this.config.getConfigArray(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalConfigArray} */
|
||||
getOptionalConfigArray(key: string): Config[] | undefined {
|
||||
return this.config.getOptionalConfigArray(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getNumber} */
|
||||
getNumber(key: string): number {
|
||||
return this.config.getNumber(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalNumber} */
|
||||
getOptionalNumber(key: string): number | undefined {
|
||||
return this.config.getOptionalNumber(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getBoolean} */
|
||||
getBoolean(key: string): boolean {
|
||||
return this.config.getBoolean(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalBoolean} */
|
||||
getOptionalBoolean(key: string): boolean | undefined {
|
||||
return this.config.getOptionalBoolean(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getString} */
|
||||
getString(key: string): string {
|
||||
return this.config.getString(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalString} */
|
||||
getOptionalString(key: string): string | undefined {
|
||||
return this.config.getOptionalString(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getStringArray} */
|
||||
getStringArray(key: string): string[] {
|
||||
return this.config.getStringArray(key);
|
||||
}
|
||||
/** {@inheritdoc @backstage/config#Config.getOptionalStringArray} */
|
||||
getOptionalStringArray(key: string): string[] | undefined {
|
||||
return this.config.getOptionalStringArray(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 { MockConfigApi } from './MockConfigApi';
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 { MockErrorApi } from './MockErrorApi';
|
||||
|
||||
async function ifResolved<T>(promise: Promise<T>): Promise<T | 'not-yet'> {
|
||||
return Promise.race([promise, Promise.resolve<'not-yet'>('not-yet')]);
|
||||
}
|
||||
|
||||
describe('MockErrorApi', () => {
|
||||
it('should throw errors by default', () => {
|
||||
const api = new MockErrorApi();
|
||||
expect(() => api.post(new Error('NOPE'))).toThrow(
|
||||
'MockErrorApi received unexpected error, Error: NOPE',
|
||||
);
|
||||
});
|
||||
|
||||
it('should collect errors', () => {
|
||||
const api = new MockErrorApi({ collect: true });
|
||||
|
||||
api.post(new Error('e1'));
|
||||
api.post(new Error('e2'), { hidden: true });
|
||||
api.post(new Error('e3'));
|
||||
|
||||
expect(api.getErrors()).toEqual([
|
||||
{
|
||||
error: new Error('e1'),
|
||||
},
|
||||
{
|
||||
error: new Error('e2'),
|
||||
context: { hidden: true },
|
||||
},
|
||||
{
|
||||
error: new Error('e3'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not emit values', async () => {
|
||||
const api = new MockErrorApi({ collect: true });
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
api.error$().subscribe({
|
||||
next({ error }) {
|
||||
reject(error);
|
||||
},
|
||||
error(error) {
|
||||
reject(error);
|
||||
},
|
||||
complete() {
|
||||
reject(new Error('observable was completed'));
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => resolve('timed-out'), 100);
|
||||
});
|
||||
|
||||
await expect(promise).resolves.toBe('timed-out');
|
||||
});
|
||||
|
||||
it('should wait for errors', async () => {
|
||||
const api = new MockErrorApi({ collect: true });
|
||||
|
||||
const wait1 = api.waitForError(/1/);
|
||||
const wait2 = api.waitForError(/2/);
|
||||
|
||||
await expect(ifResolved(wait1)).resolves.toBe('not-yet');
|
||||
await expect(ifResolved(wait2)).resolves.toBe('not-yet');
|
||||
api.post(new Error('e0'));
|
||||
await expect(ifResolved(wait1)).resolves.toBe('not-yet');
|
||||
await expect(ifResolved(wait2)).resolves.toBe('not-yet');
|
||||
api.post(new Error('e1'));
|
||||
await expect(ifResolved(wait1)).resolves.toEqual({
|
||||
error: new Error('e1'),
|
||||
});
|
||||
await expect(ifResolved(wait2)).resolves.toBe('not-yet');
|
||||
api.post(new Error('e2'), { hidden: true });
|
||||
await expect(ifResolved(wait2)).resolves.toEqual({
|
||||
error: new Error('e2'),
|
||||
context: { hidden: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should time out waiting for error', async () => {
|
||||
const api = new MockErrorApi({ collect: true });
|
||||
|
||||
await expect(api.waitForError(/1/, 1)).rejects.toThrow(
|
||||
'Timed out waiting for error',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 {
|
||||
ErrorApi,
|
||||
ErrorApiError,
|
||||
ErrorApiErrorContext,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* Constructor arguments for {@link MockErrorApi}
|
||||
* @public
|
||||
*/
|
||||
export type MockErrorApiOptions = {
|
||||
// Need to be true if getErrors is used in testing.
|
||||
collect?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* ErrorWithContext contains error and ErrorApiErrorContext
|
||||
* @public
|
||||
*/
|
||||
export type ErrorWithContext = {
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
};
|
||||
|
||||
type Waiter = {
|
||||
pattern: RegExp;
|
||||
resolve: (err: ErrorWithContext) => void;
|
||||
};
|
||||
|
||||
const nullObservable = {
|
||||
subscribe: () => ({ unsubscribe: () => {}, closed: true }),
|
||||
|
||||
[Symbol.observable]() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock implementation of the {@link core-plugin-api#ErrorApi} to be used in tests.
|
||||
* Includes withForError and getErrors methods for error testing.
|
||||
* @public
|
||||
*/
|
||||
export class MockErrorApi implements ErrorApi {
|
||||
private readonly errors = new Array<ErrorWithContext>();
|
||||
private readonly waiters = new Set<Waiter>();
|
||||
|
||||
constructor(private readonly options: MockErrorApiOptions = {}) {}
|
||||
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext) {
|
||||
if (this.options.collect) {
|
||||
this.errors.push({ error, context });
|
||||
|
||||
for (const waiter of this.waiters) {
|
||||
if (waiter.pattern.test(error.message)) {
|
||||
this.waiters.delete(waiter);
|
||||
waiter.resolve({ error, context });
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`MockErrorApi received unexpected error, ${error}`);
|
||||
}
|
||||
|
||||
error$(): Observable<{
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
}> {
|
||||
return nullObservable;
|
||||
}
|
||||
|
||||
getErrors(): ErrorWithContext[] {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
waitForError(
|
||||
pattern: RegExp,
|
||||
timeoutMs: number = 2000,
|
||||
): Promise<ErrorWithContext> {
|
||||
return new Promise<ErrorWithContext>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out waiting for error'));
|
||||
}, timeoutMs);
|
||||
|
||||
this.waiters.add({ resolve, pattern });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { MockErrorApi } from './MockErrorApi';
|
||||
export type { MockErrorApiOptions, ErrorWithContext } from './MockErrorApi';
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2022 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 { rest } from 'msw';
|
||||
import { setupServer } from 'msw';
|
||||
import { registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { MockFetchApi } from './MockFetchApi';
|
||||
|
||||
describe('MockFetchApi', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
it('works with default constructor', async () => {
|
||||
worker.use(
|
||||
rest.get('http://example.com/data.json', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ a: 'foo' })),
|
||||
),
|
||||
);
|
||||
const m = new MockFetchApi();
|
||||
const response = await m.fetch('http://example.com/data.json');
|
||||
await expect(response.json()).resolves.toEqual({ a: 'foo' });
|
||||
});
|
||||
|
||||
describe('baseImplementation', () => {
|
||||
it('works with a mock implementation', async () => {
|
||||
const inner = jest.fn();
|
||||
const m = new MockFetchApi({ baseImplementation: inner });
|
||||
await m.fetch('http://example.com/data.json');
|
||||
expect(inner).toHaveBeenLastCalledWith('http://example.com/data.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePluginProtocol', () => {
|
||||
it('works', async () => {
|
||||
const inner = jest.fn();
|
||||
const m = new MockFetchApi({
|
||||
baseImplementation: inner,
|
||||
resolvePluginProtocol: {
|
||||
discoveryApi: {
|
||||
getBaseUrl: async id => `https://blah.com/api/${id}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await m.fetch('plugin://the-plugin/a/data.json');
|
||||
expect(inner.mock.calls[0][0]).toBe(
|
||||
'https://blah.com/api/the-plugin/a/data.json',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectIdentityAuth', () => {
|
||||
it('works with token', async () => {
|
||||
const inner = jest.fn();
|
||||
const m = new MockFetchApi({
|
||||
baseImplementation: inner,
|
||||
injectIdentityAuth: { token: 'hello' },
|
||||
});
|
||||
await m.fetch('http://example.com/data.json');
|
||||
expect(inner.mock.calls[0][0].headers?.get('authorization')).toBe(
|
||||
'Bearer hello',
|
||||
);
|
||||
});
|
||||
|
||||
it('works with identityApi', async () => {
|
||||
const inner = jest.fn();
|
||||
const m = new MockFetchApi({
|
||||
baseImplementation: inner,
|
||||
injectIdentityAuth: {
|
||||
identityApi: {
|
||||
async getCredentials() {
|
||||
return { token: 'hello2' };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await m.fetch('http://example.com/data.json');
|
||||
expect(inner.mock.calls[0][0].headers?.get('authorization')).toBe(
|
||||
'Bearer hello2',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
createFetchApi,
|
||||
FetchMiddleware,
|
||||
FetchMiddlewares,
|
||||
} from '@backstage/core-app-api';
|
||||
import {
|
||||
DiscoveryApi,
|
||||
FetchApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import crossFetch, { Response } from 'cross-fetch';
|
||||
|
||||
/**
|
||||
* The options given when constructing a {@link MockFetchApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MockFetchApiOptions {
|
||||
/**
|
||||
* Define the underlying base `fetch` implementation.
|
||||
*
|
||||
* @defaultValue undefined
|
||||
* @remarks
|
||||
*
|
||||
* Leaving out this parameter or passing `undefined`, makes the API use the
|
||||
* global `fetch` implementation to make real network requests.
|
||||
*
|
||||
* `'none'` swallows all calls and makes no requests at all.
|
||||
*
|
||||
* You can also pass in any `fetch` compatible callback, such as a
|
||||
* `jest.fn()`, if you want to use a custom implementation or to just track
|
||||
* and assert on calls.
|
||||
*/
|
||||
baseImplementation?: undefined | 'none' | typeof crossFetch;
|
||||
|
||||
/**
|
||||
* Add translation from `plugin://` URLs to concrete http(s) URLs, basically
|
||||
* simulating what
|
||||
* {@link @backstage/core-app-api#FetchMiddlewares.resolvePluginProtocol}
|
||||
* does.
|
||||
*
|
||||
* @defaultValue undefined
|
||||
* @remarks
|
||||
*
|
||||
* Leaving out this parameter or passing `undefined`, disables plugin protocol
|
||||
* translation.
|
||||
*
|
||||
* To enable the feature, pass in a discovery API which is then used to
|
||||
* resolve the URLs.
|
||||
*/
|
||||
resolvePluginProtocol?:
|
||||
| undefined
|
||||
| { discoveryApi: Pick<DiscoveryApi, 'getBaseUrl'> };
|
||||
|
||||
/**
|
||||
* Add token based Authorization headers to requests, basically simulating
|
||||
* what {@link @backstage/core-app-api#FetchMiddlewares.injectIdentityAuth}
|
||||
* does.
|
||||
*
|
||||
* @defaultValue undefined
|
||||
* @remarks
|
||||
*
|
||||
* Leaving out this parameter or passing `undefined`, disables auth injection.
|
||||
*
|
||||
* To enable the feature, pass in either a static token or an identity API
|
||||
* which is queried on each request for a token.
|
||||
*/
|
||||
injectIdentityAuth?:
|
||||
| undefined
|
||||
| { token: string }
|
||||
| { identityApi: Pick<IdentityApi, 'getCredentials'> };
|
||||
}
|
||||
|
||||
/**
|
||||
* A test helper implementation of {@link @backstage/core-plugin-api#FetchApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MockFetchApi implements FetchApi {
|
||||
private readonly implementation: FetchApi;
|
||||
|
||||
/**
|
||||
* Creates a mock {@link @backstage/core-plugin-api#FetchApi}.
|
||||
*/
|
||||
constructor(options?: MockFetchApiOptions) {
|
||||
this.implementation = build(options);
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#FetchApi.fetch} */
|
||||
get fetch(): typeof crossFetch {
|
||||
return this.implementation.fetch;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
function build(options?: MockFetchApiOptions): FetchApi {
|
||||
return createFetchApi({
|
||||
baseImplementation: baseImplementation(options),
|
||||
middleware: [
|
||||
resolvePluginProtocol(options),
|
||||
injectIdentityAuth(options),
|
||||
].filter((x): x is FetchMiddleware => Boolean(x)),
|
||||
});
|
||||
}
|
||||
|
||||
function baseImplementation(
|
||||
options: MockFetchApiOptions | undefined,
|
||||
): typeof crossFetch {
|
||||
const implementation = options?.baseImplementation;
|
||||
if (!implementation) {
|
||||
// Return a wrapper that evaluates global.fetch at call time, not construction time.
|
||||
// This allows MSW to patch global.fetch after MockFetchApi is constructed.
|
||||
return (input, init) => global.fetch(input, init);
|
||||
} else if (implementation === 'none') {
|
||||
return () => Promise.resolve(new Response());
|
||||
}
|
||||
return implementation;
|
||||
}
|
||||
|
||||
function resolvePluginProtocol(
|
||||
allOptions: MockFetchApiOptions | undefined,
|
||||
): FetchMiddleware | undefined {
|
||||
const options = allOptions?.resolvePluginProtocol;
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return FetchMiddlewares.resolvePluginProtocol({
|
||||
discoveryApi: options.discoveryApi,
|
||||
});
|
||||
}
|
||||
|
||||
function injectIdentityAuth(
|
||||
allOptions: MockFetchApiOptions | undefined,
|
||||
): FetchMiddleware | undefined {
|
||||
const options = allOptions?.injectIdentityAuth;
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identityApi: Pick<IdentityApi, 'getCredentials'> =
|
||||
'token' in options
|
||||
? { getCredentials: async () => ({ token: options.token }) }
|
||||
: options.identityApi;
|
||||
|
||||
return FetchMiddlewares.injectIdentityAuth({
|
||||
identityApi: identityApi as IdentityApi,
|
||||
allowUrl: () => true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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 { MockFetchApi } from './MockFetchApi';
|
||||
export type { MockFetchApiOptions } from './MockFetchApi';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
AuthorizeResult,
|
||||
createPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { MockPermissionApi } from './MockPermissionApi';
|
||||
|
||||
describe('MockPermissionApi', () => {
|
||||
it('returns ALLOW by default', async () => {
|
||||
const api = new MockPermissionApi();
|
||||
|
||||
await expect(
|
||||
api.authorize({
|
||||
permission: createPermission({ name: 'permission.1', attributes: {} }),
|
||||
}),
|
||||
).resolves.toEqual({ result: AuthorizeResult.ALLOW });
|
||||
});
|
||||
|
||||
it('allows passing a handler to customize the result', async () => {
|
||||
const api = new MockPermissionApi(request =>
|
||||
request.permission.name === 'permission.2'
|
||||
? AuthorizeResult.DENY
|
||||
: AuthorizeResult.ALLOW,
|
||||
);
|
||||
|
||||
await expect(
|
||||
api.authorize({
|
||||
permission: createPermission({ name: 'permission.2', attributes: {} }),
|
||||
}),
|
||||
).resolves.toEqual({ result: AuthorizeResult.DENY });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2022 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 { PermissionApi } from '@backstage/plugin-permission-react';
|
||||
import {
|
||||
EvaluatePermissionResponse,
|
||||
EvaluatePermissionRequest,
|
||||
AuthorizeResult,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
|
||||
/**
|
||||
* Mock implementation of
|
||||
* {@link @backstage/plugin-permission-react#PermissionApi}. Supply a
|
||||
* requestHandler function to override the mock result returned for a given
|
||||
* request.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MockPermissionApi implements PermissionApi {
|
||||
constructor(
|
||||
private readonly requestHandler: (
|
||||
request: EvaluatePermissionRequest,
|
||||
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY = () =>
|
||||
AuthorizeResult.ALLOW,
|
||||
) {}
|
||||
|
||||
async authorize(
|
||||
request: EvaluatePermissionRequest,
|
||||
): Promise<EvaluatePermissionResponse> {
|
||||
return { result: this.requestHandler(request) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 { MockPermissionApi } from './MockPermissionApi';
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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 { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { MockStorageApi } from './MockStorageApi';
|
||||
|
||||
describe('WebStorage Storage API', () => {
|
||||
const createMockStorage = (): StorageApi => {
|
||||
return MockStorageApi.create();
|
||||
};
|
||||
|
||||
it('should return undefined for values which are unset', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
expect(storage.snapshot('myfakekey').value).toBeUndefined();
|
||||
expect(storage.snapshot('myfakekey')).toEqual({
|
||||
key: 'myfakekey',
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
newValue: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow the setting and snapshotting of the simple data structures', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
await storage.set('myfakekey', 'helloimastring');
|
||||
await storage.set('mysecondfakekey', 1234);
|
||||
await storage.set('mythirdfakekey', true);
|
||||
expect(storage.snapshot('myfakekey').value).toBe('helloimastring');
|
||||
expect(storage.snapshot('mysecondfakekey').value).toBe(1234);
|
||||
expect(storage.snapshot('mythirdfakekey').value).toBe(true);
|
||||
expect(storage.snapshot('myfakekey')).toEqual({
|
||||
key: 'myfakekey',
|
||||
presence: 'present',
|
||||
value: 'helloimastring',
|
||||
});
|
||||
expect(storage.snapshot('mysecondfakekey')).toEqual({
|
||||
key: 'mysecondfakekey',
|
||||
presence: 'present',
|
||||
value: 1234,
|
||||
});
|
||||
expect(storage.snapshot('mythirdfakekey')).toEqual({
|
||||
key: 'mythirdfakekey',
|
||||
presence: 'present',
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow setting of complex datastructures', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
const mockData = {
|
||||
something: 'here',
|
||||
is: [{ super: { complex: [{ but: 'something', why: true }] } }],
|
||||
};
|
||||
|
||||
await storage.set('myfakekey', mockData);
|
||||
|
||||
expect(storage.snapshot('myfakekey').value).toEqual(mockData);
|
||||
expect(storage.snapshot('myfakekey')).toEqual({
|
||||
key: 'myfakekey',
|
||||
presence: 'present',
|
||||
value: mockData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should subscribe to key changes when setting a new value', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
const wrongKeyNextHandler = jest.fn();
|
||||
const selectedKeyNextHandler = jest.fn();
|
||||
const mockData = { hello: 'im a great new value' };
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
storage.observe$<typeof mockData>('correctKey').subscribe({
|
||||
next: (...args) => {
|
||||
selectedKeyNextHandler(...args);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler });
|
||||
|
||||
storage.set('correctKey', mockData);
|
||||
});
|
||||
|
||||
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
|
||||
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
|
||||
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
|
||||
key: 'correctKey',
|
||||
presence: 'present',
|
||||
value: mockData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should subscribe to key changes when deleting a value', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
const wrongKeyNextHandler = jest.fn();
|
||||
const selectedKeyNextHandler = jest.fn();
|
||||
const mockData = { hello: 'im a great new value' };
|
||||
|
||||
storage.set('correctKey', mockData);
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
storage.observe$('correctKey').subscribe({
|
||||
next: (...args) => {
|
||||
selectedKeyNextHandler(...args);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler });
|
||||
|
||||
storage.remove('correctKey');
|
||||
});
|
||||
|
||||
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
|
||||
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
|
||||
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
|
||||
key: 'correctKey',
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
newValue: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to create different buckets for different uses', async () => {
|
||||
const rootStorage = createMockStorage();
|
||||
|
||||
const firstStorage = rootStorage.forBucket('userSettings');
|
||||
const secondStorage = rootStorage.forBucket('profileSettings');
|
||||
const keyName = 'blobby';
|
||||
|
||||
await firstStorage.set(keyName, 'boop');
|
||||
await secondStorage.set(keyName, 'deerp');
|
||||
|
||||
expect(firstStorage.snapshot(keyName)).not.toBe(
|
||||
secondStorage.snapshot(keyName),
|
||||
);
|
||||
expect(firstStorage.snapshot(keyName).value).toBe('boop');
|
||||
expect(secondStorage.snapshot(keyName).value).toBe('deerp');
|
||||
expect(firstStorage.snapshot(keyName)).not.toEqual(
|
||||
secondStorage.snapshot(keyName),
|
||||
);
|
||||
expect(firstStorage.snapshot(keyName)).toEqual({
|
||||
key: keyName,
|
||||
presence: 'present',
|
||||
value: 'boop',
|
||||
});
|
||||
expect(secondStorage.snapshot(keyName)).toEqual({
|
||||
key: keyName,
|
||||
presence: 'present',
|
||||
value: 'deerp',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clash with other namespaces when creating buckets', async () => {
|
||||
const rootStorage = createMockStorage();
|
||||
|
||||
// when getting key test2 it will translate to /profile/something/deep/test2
|
||||
const firstStorage = rootStorage
|
||||
.forBucket('profile')
|
||||
.forBucket('something')
|
||||
.forBucket('deep');
|
||||
// when getting key deep/test2 it will translate to /profile/something/deep/test2
|
||||
const secondStorage = rootStorage.forBucket('profile/something');
|
||||
|
||||
await firstStorage.set('test2', { error: true });
|
||||
|
||||
expect(secondStorage.snapshot('deep/test2').value).toBe(undefined);
|
||||
expect(secondStorage.snapshot('deep/test2')).toMatchObject({
|
||||
presence: 'absent',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not reuse storage instances between different rootStorages', async () => {
|
||||
const rootStorage1 = createMockStorage();
|
||||
const rootStorage2 = createMockStorage();
|
||||
|
||||
const firstStorage = rootStorage1.forBucket('something');
|
||||
const secondStorage = rootStorage2.forBucket('something');
|
||||
|
||||
await firstStorage.set('test2', true);
|
||||
|
||||
expect(firstStorage.snapshot('test2').value).toBe(true);
|
||||
expect(secondStorage.snapshot('test2').value).toBe(undefined);
|
||||
expect(firstStorage.snapshot('test2')).toEqual({
|
||||
key: 'test2',
|
||||
presence: 'present',
|
||||
value: true,
|
||||
});
|
||||
expect(secondStorage.snapshot('test2')).toEqual({
|
||||
key: 'test2',
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should freeze the snapshot value', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
const data = { foo: 'bar', baz: [{ foo: 'bar' }] };
|
||||
storage.set('foo', data);
|
||||
|
||||
const snapshot = storage.snapshot<typeof data>('foo');
|
||||
expect(snapshot.value).not.toBe(data);
|
||||
|
||||
if (snapshot.presence !== 'present') {
|
||||
throw new Error('Invalid presence');
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
snapshot.value.foo = 'buzz';
|
||||
}).toThrow(/Cannot assign to read only property/);
|
||||
expect(() => {
|
||||
snapshot.value.baz[0].foo = 'buzz';
|
||||
}).toThrow(/Cannot assign to read only property/);
|
||||
expect(() => {
|
||||
snapshot.value.baz.push({ foo: 'buzz' });
|
||||
}).toThrow(/Cannot add property 1, object is not extensible/);
|
||||
});
|
||||
|
||||
it('should freeze observed values', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
const snapshotPromise = new Promise<any>(resolve => {
|
||||
storage.observe$('test').subscribe({
|
||||
next: resolve,
|
||||
});
|
||||
});
|
||||
|
||||
storage.set('test', {
|
||||
foo: {
|
||||
bar: 'baz',
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await snapshotPromise;
|
||||
expect(snapshot.presence).toBe('present');
|
||||
expect(() => {
|
||||
snapshot.value!.foo.bar = 'qux';
|
||||
}).toThrow(/Cannot assign to read only property 'bar' of object/);
|
||||
});
|
||||
|
||||
it('should JSON serialize stored values', async () => {
|
||||
const storage = createMockStorage();
|
||||
|
||||
storage.set<any>('test', {
|
||||
foo: {
|
||||
toJSON() {
|
||||
return {
|
||||
bar: 'baz',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(storage.snapshot('test')).toMatchObject({
|
||||
presence: 'present',
|
||||
value: {
|
||||
foo: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 { StorageApi, StorageValueSnapshot } from '@backstage/core-plugin-api';
|
||||
import { JsonValue, Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
/**
|
||||
* Type for map holding data in {@link MockStorageApi}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MockStorageBucket = { [key: string]: any };
|
||||
|
||||
/**
|
||||
* Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MockStorageApi implements StorageApi {
|
||||
private readonly namespace: string;
|
||||
private readonly data: MockStorageBucket;
|
||||
private readonly bucketStorageApis: Map<string, MockStorageApi>;
|
||||
|
||||
private constructor(
|
||||
namespace: string,
|
||||
bucketStorageApis: Map<string, MockStorageApi>,
|
||||
data?: MockStorageBucket,
|
||||
) {
|
||||
this.namespace = namespace;
|
||||
this.bucketStorageApis = bucketStorageApis;
|
||||
this.data = { ...data };
|
||||
}
|
||||
|
||||
static create(data?: MockStorageBucket) {
|
||||
// Translate a nested data object structure into a flat object with keys
|
||||
// like `/a/b` with their corresponding leaf values
|
||||
const keyValues: { [key: string]: any } = {};
|
||||
function put(value: { [key: string]: any }, namespace: string) {
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
put(val, `${namespace}/${key}`);
|
||||
} else {
|
||||
const namespacedKey = `${namespace}/${key.replace(/^\//, '')}`;
|
||||
keyValues[namespacedKey] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
put(data ?? {}, '');
|
||||
return new MockStorageApi('', new Map(), keyValues);
|
||||
}
|
||||
|
||||
forBucket(name: string): StorageApi {
|
||||
if (!this.bucketStorageApis.has(name)) {
|
||||
this.bucketStorageApis.set(
|
||||
name,
|
||||
new MockStorageApi(
|
||||
`${this.namespace}/${name}`,
|
||||
this.bucketStorageApis,
|
||||
this.data,
|
||||
),
|
||||
);
|
||||
}
|
||||
return this.bucketStorageApis.get(name)!;
|
||||
}
|
||||
|
||||
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {
|
||||
if (this.data.hasOwnProperty(this.getKeyName(key))) {
|
||||
const data = this.data[this.getKeyName(key)];
|
||||
return {
|
||||
key,
|
||||
presence: 'present',
|
||||
value: data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key,
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async set<T>(key: string, data: T): Promise<void> {
|
||||
const serialized = JSON.parse(JSON.stringify(data), (_key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
this.data[this.getKeyName(key)] = serialized;
|
||||
this.notifyChanges({
|
||||
key,
|
||||
presence: 'present',
|
||||
value: serialized,
|
||||
});
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
delete this.data[this.getKeyName(key)];
|
||||
this.notifyChanges({
|
||||
key,
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
observe$<T extends JsonValue>(
|
||||
key: string,
|
||||
): Observable<StorageValueSnapshot<T>> {
|
||||
return this.observable.filter(({ key: messageKey }) => messageKey === key);
|
||||
}
|
||||
|
||||
private getKeyName(key: string) {
|
||||
return `${this.namespace}/${encodeURIComponent(key)}`;
|
||||
}
|
||||
|
||||
private notifyChanges<T extends JsonValue>(message: StorageValueSnapshot<T>) {
|
||||
for (const subscription of this.subscribers) {
|
||||
subscription.next(message);
|
||||
}
|
||||
}
|
||||
|
||||
private subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>
|
||||
>();
|
||||
|
||||
private readonly observable = new ObservableImpl<
|
||||
StorageValueSnapshot<JsonValue>
|
||||
>(subscriber => {
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { MockStorageApi } from './MockStorageApi';
|
||||
export type { MockStorageBucket } from './MockStorageApi';
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { MockTranslationApi } from './MockTranslationApi';
|
||||
|
||||
describe('MockTranslationApi', () => {
|
||||
function snapshotWithMessages<
|
||||
const TMessages extends { [key in string]: string },
|
||||
>(messages: TMessages) {
|
||||
const translationApi = MockTranslationApi.create();
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages,
|
||||
});
|
||||
const snapshot = translationApi.getTranslation(ref);
|
||||
if (!snapshot.ready) {
|
||||
throw new Error('Translation snapshot is not ready');
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
it('should format plain messages', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
foo: 'Foo',
|
||||
bar: 'Bar',
|
||||
baz: 'Baz',
|
||||
});
|
||||
|
||||
expect(snapshot.t('foo')).toBe('Foo');
|
||||
expect(snapshot.t('bar')).toBe('Bar');
|
||||
expect(snapshot.t('baz')).toBe('Baz');
|
||||
});
|
||||
|
||||
it('should support interpolation', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
shallow: 'Foo {{ bar }}',
|
||||
multiple: 'Foo {{ bar }} {{ baz }}',
|
||||
deep: 'Foo {{ bar.baz }}',
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.t('shallow')).toBe('Foo {{ bar }}');
|
||||
expect(snapshot.t('shallow', { bar: 'Bar' })).toBe('Foo Bar');
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.t('multiple')).toBe('Foo {{ bar }} {{ baz }}');
|
||||
// @ts-expect-error
|
||||
expect(snapshot.t('multiple', { bar: 'Bar' })).toBe('Foo Bar {{ baz }}');
|
||||
expect(snapshot.t('multiple', { bar: 'Bar', baz: 'Baz' })).toBe(
|
||||
'Foo Bar Baz',
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.t('deep')).toBe('Foo {{ bar.baz }}');
|
||||
expect(snapshot.t('deep', { bar: { baz: 'Baz' } })).toBe('Foo Baz');
|
||||
});
|
||||
|
||||
// Escaping isn't as useful in React, since we don't need to escape HTML in strings
|
||||
it('should not escape by default', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
foo: 'Foo {{ foo }}',
|
||||
});
|
||||
|
||||
expect(snapshot.t('foo', { foo: '<div>' })).toBe('Foo <div>');
|
||||
expect(
|
||||
snapshot.t('foo', {
|
||||
foo: '<div>',
|
||||
interpolation: { escapeValue: true },
|
||||
}),
|
||||
).toBe('Foo <div>');
|
||||
});
|
||||
|
||||
it('should support nesting', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
foo: 'Foo $t(bar) $t(baz)',
|
||||
bar: 'Nested',
|
||||
baz: 'Baz {{ qux }}',
|
||||
});
|
||||
|
||||
expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep');
|
||||
});
|
||||
|
||||
it('should support jsx interpolation', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
empty: 'derp',
|
||||
jsx: '={{ x }}',
|
||||
jsxNested: '={{ x.y.z }}',
|
||||
jsxDeep: '<$t(jsx)>',
|
||||
});
|
||||
|
||||
expect(snapshot.t('jsx', { x: <h1>hello</h1> })).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
=
|
||||
<h1>
|
||||
hello
|
||||
</h1>
|
||||
</React.Fragment>
|
||||
`);
|
||||
expect(snapshot.t('jsx', { replace: { x: <h1>hello</h1> } }))
|
||||
.toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
=
|
||||
<h1>
|
||||
hello
|
||||
</h1>
|
||||
</React.Fragment>
|
||||
`);
|
||||
expect(
|
||||
snapshot.t('jsxNested', { replace: { x: { y: { z: <h1>hello</h1> } } } }),
|
||||
).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
=
|
||||
<h1>
|
||||
hello
|
||||
</h1>
|
||||
</React.Fragment>
|
||||
`);
|
||||
expect(snapshot.t('jsxDeep', { x: <h1>hello</h1> })).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
<=
|
||||
<h1>
|
||||
hello
|
||||
</h1>
|
||||
>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support formatting', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
plain: '= {{ x }}',
|
||||
number: '= {{ x, number }}',
|
||||
numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}',
|
||||
relativeTime: '= {{ x, relativeTime }}',
|
||||
relativeSeconds: '= {{ x, relativeTime(second) }}',
|
||||
relativeSecondsShort:
|
||||
'= {{ x, relativeTime(range: second; style: short) }}',
|
||||
list: '= {{ x, list }}',
|
||||
});
|
||||
|
||||
expect(snapshot.t('plain', { x: '5' })).toBe('= 5');
|
||||
expect(snapshot.t('number', { x: 5 })).toBe('= 5');
|
||||
expect(
|
||||
snapshot.t('number', {
|
||||
x: 5,
|
||||
formatParams: { x: { minimumFractionDigits: 1 } },
|
||||
}),
|
||||
).toBe('= 5.0');
|
||||
expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00');
|
||||
expect(
|
||||
snapshot.t('numberFixed', {
|
||||
x: 5,
|
||||
formatParams: { x: { minimumFractionDigits: 3 } },
|
||||
}),
|
||||
).toBe('= 5.000');
|
||||
expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days');
|
||||
expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago');
|
||||
expect(
|
||||
snapshot.t('relativeTime', {
|
||||
x: 15,
|
||||
formatParams: { x: { range: 'weeks' } },
|
||||
}),
|
||||
).toBe('= in 15 weeks');
|
||||
expect(
|
||||
snapshot.t('relativeTime', {
|
||||
x: 15,
|
||||
formatParams: { x: { range: 'weeks', style: 'short' } },
|
||||
}),
|
||||
).toBe('= in 15 wk.');
|
||||
expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second');
|
||||
expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds');
|
||||
expect(snapshot.t('relativeSeconds', { x: -3 })).toBe('= 3 seconds ago');
|
||||
expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds');
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe('= in 1 sec.');
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe('= in 2 sec.');
|
||||
expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe('= 3 sec. ago');
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe('= in 0 sec.');
|
||||
expect(snapshot.t('list', { x: ['a'] })).toBe('= a');
|
||||
expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b');
|
||||
expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe('= a, b, and c');
|
||||
});
|
||||
|
||||
it('should support plurals', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
derp_one: 'derp',
|
||||
derp_other: 'derps',
|
||||
derpWithCount_one: '{{ count }} derp',
|
||||
derpWithCount_other: '{{ count }} derps',
|
||||
});
|
||||
|
||||
expect(snapshot.t('derp', { count: 1 })).toBe('derp');
|
||||
expect(snapshot.t('derp', { count: 2 })).toBe('derps');
|
||||
expect(snapshot.t('derp', { count: 0 })).toBe('derps');
|
||||
expect(snapshot.t('derpWithCount', { count: 1 })).toBe('1 derp');
|
||||
expect(snapshot.t('derpWithCount', { count: 2 })).toBe('2 derps');
|
||||
expect(snapshot.t('derpWithCount', { count: 0 })).toBe('0 derps');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 {
|
||||
TranslationApi,
|
||||
TranslationRef,
|
||||
TranslationSnapshot,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { createInstance as createI18n, type i18n as I18n } from 'i18next';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
import { Observable } from '@backstage/types';
|
||||
// Internal import to avoid code duplication, this will lead to duplication in build output
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalTranslationRef } from '../../../../frontend-plugin-api/src/translation/TranslationRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { JsxInterpolator } from '../../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
/**
|
||||
* Mock implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MockTranslationApi implements TranslationApi {
|
||||
static create() {
|
||||
const i18n = createI18n({
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
supportedLngs: [DEFAULT_LANGUAGE],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
// Used for the JsxInterpolator format hook
|
||||
alwaysFormat: true,
|
||||
},
|
||||
ns: [],
|
||||
defaultNS: false,
|
||||
fallbackNS: false,
|
||||
|
||||
// Disable resource loading on init, meaning i18n will be ready to use immediately
|
||||
initImmediate: false,
|
||||
});
|
||||
|
||||
i18n.init();
|
||||
if (!i18n.isInitialized) {
|
||||
throw new Error('i18next was unexpectedly not initialized');
|
||||
}
|
||||
|
||||
const interpolator = JsxInterpolator.fromI18n(i18n);
|
||||
|
||||
return new MockTranslationApi(i18n, interpolator);
|
||||
}
|
||||
|
||||
#i18n: I18n;
|
||||
#interpolator: JsxInterpolator;
|
||||
#registeredRefs = new Set<string>();
|
||||
|
||||
private constructor(i18n: I18n, interpolator: JsxInterpolator) {
|
||||
this.#i18n = i18n;
|
||||
this.#interpolator = interpolator;
|
||||
}
|
||||
|
||||
getTranslation<TMessages extends { [key in string]: string }>(
|
||||
translationRef: TranslationRef<string, TMessages>,
|
||||
): TranslationSnapshot<TMessages> {
|
||||
const internalRef = toInternalTranslationRef(translationRef);
|
||||
|
||||
if (!this.#registeredRefs.has(internalRef.id)) {
|
||||
this.#registeredRefs.add(internalRef.id);
|
||||
this.#i18n.addResourceBundle(
|
||||
DEFAULT_LANGUAGE,
|
||||
internalRef.id,
|
||||
internalRef.getDefaultMessages(),
|
||||
false, // do not merge
|
||||
true, // overwrite existing
|
||||
);
|
||||
}
|
||||
|
||||
const t = this.#interpolator.wrapT<TMessages>(
|
||||
this.#i18n.getFixedT(null, internalRef.id),
|
||||
);
|
||||
|
||||
return {
|
||||
ready: true,
|
||||
t,
|
||||
};
|
||||
}
|
||||
|
||||
translation$<TMessages extends { [key in string]: string }>(): Observable<
|
||||
TranslationSnapshot<TMessages>
|
||||
> {
|
||||
// No need to implement, getTranslation will always return a ready snapshot
|
||||
return new ObservableImpl<TranslationSnapshot<TMessages>>(_subscriber => {
|
||||
return () => {};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export { MockTranslationApi } from './MockTranslationApi';
|
||||
@@ -14,19 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
MockConfigApi,
|
||||
type ErrorWithContext,
|
||||
MockErrorApi,
|
||||
type MockErrorApiOptions,
|
||||
MockFetchApi,
|
||||
type MockFetchApiOptions,
|
||||
MockPermissionApi,
|
||||
MockStorageApi,
|
||||
type MockStorageBucket,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi';
|
||||
export { mockApis } from './mockApis';
|
||||
export {
|
||||
type MockApiFactorySymbol,
|
||||
@@ -40,6 +27,30 @@ export {
|
||||
*/
|
||||
export type { MockAlertApi } from './AlertApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockAnalyticsApi } from './AnalyticsApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockConfigApi } from './ConfigApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type {
|
||||
MockErrorApi,
|
||||
MockErrorApiOptions,
|
||||
ErrorWithContext,
|
||||
} from './ErrorApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockFetchApi, MockFetchApiOptions } from './FetchApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -47,3 +58,18 @@ export type {
|
||||
MockFeatureFlagsApi,
|
||||
MockFeatureFlagsApiOptions,
|
||||
} from './FeatureFlagsApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockPermissionApi } from './PermissionApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockStorageApi, MockStorageBucket } from './StorageApi';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type { MockTranslationApi } from './TranslationApi';
|
||||
|
||||
@@ -16,15 +16,46 @@
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
analyticsApiRef,
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
errorApiRef,
|
||||
fetchApiRef,
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
storageApiRef,
|
||||
translationApiRef,
|
||||
type AnalyticsApi,
|
||||
type ConfigApi,
|
||||
type DiscoveryApi,
|
||||
type ErrorApi,
|
||||
type FetchApi,
|
||||
type IdentityApi,
|
||||
type StorageApi,
|
||||
type TranslationApi,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { mockApis as testUtilsMockApis } from '@backstage/test-utils';
|
||||
import {
|
||||
permissionApiRef,
|
||||
type PermissionApi,
|
||||
} from '@backstage/plugin-permission-react';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
EvaluatePermissionRequest,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { MockAlertApi } from './AlertApi';
|
||||
import {
|
||||
MockFeatureFlagsApi,
|
||||
MockFeatureFlagsApiOptions,
|
||||
} from './FeatureFlagsApi';
|
||||
import { MockAnalyticsApi } from './AnalyticsApi';
|
||||
import { MockConfigApi } from './ConfigApi';
|
||||
import { MockErrorApi, MockErrorApiOptions } from './ErrorApi';
|
||||
import { MockFetchApi, MockFetchApiOptions } from './FetchApi';
|
||||
import { MockStorageApi } from './StorageApi';
|
||||
import { MockPermissionApi } from './PermissionApi';
|
||||
import { MockTranslationApi } from './TranslationApi';
|
||||
import {
|
||||
ApiMock,
|
||||
mockWithApiFactory,
|
||||
@@ -211,12 +242,289 @@ export namespace mockApis {
|
||||
}));
|
||||
}
|
||||
|
||||
// Re-export all mockApis from test-utils
|
||||
export const analytics = testUtilsMockApis.analytics;
|
||||
export const config = testUtilsMockApis.config;
|
||||
export const discovery = testUtilsMockApis.discovery;
|
||||
export const identity = testUtilsMockApis.identity;
|
||||
export const permission = testUtilsMockApis.permission;
|
||||
export const storage = testUtilsMockApis.storage;
|
||||
export const translation = testUtilsMockApis.translation;
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#AnalyticsApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function analytics(): MockAnalyticsApi &
|
||||
MockWithApiFactory<AnalyticsApi> {
|
||||
const instance = new MockAnalyticsApi();
|
||||
return mockWithApiFactory(analyticsApiRef, instance) as MockAnalyticsApi &
|
||||
MockWithApiFactory<AnalyticsApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#AnalyticsApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace analytics {
|
||||
export const mock = simpleMock(analyticsApiRef, () => ({
|
||||
captureEvent: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}.
|
||||
* By default returns the default translation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function translation(): MockTranslationApi &
|
||||
MockWithApiFactory<TranslationApi> {
|
||||
const instance = MockTranslationApi.create();
|
||||
return mockWithApiFactory(
|
||||
translationApiRef,
|
||||
instance,
|
||||
) as MockTranslationApi & MockWithApiFactory<TranslationApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api/alpha#TranslationApi}.
|
||||
*
|
||||
* @see {@link @backstage/frontend-plugin-api#mockApis.translation}
|
||||
* @public
|
||||
*/
|
||||
export namespace translation {
|
||||
/**
|
||||
* Creates a mock of {@link @backstage/core-plugin-api/alpha#TranslationApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const mock = simpleMock(translationApiRef, () => ({
|
||||
getTranslation: jest.fn(),
|
||||
translation$: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#ConfigApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function config(options?: {
|
||||
data?: JsonObject;
|
||||
}): MockConfigApi & MockWithApiFactory<ConfigApi> {
|
||||
const instance = new MockConfigApi(options?.data ?? {});
|
||||
return mockWithApiFactory(configApiRef, instance) as MockConfigApi &
|
||||
MockWithApiFactory<ConfigApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#ConfigApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace config {
|
||||
export const mock = simpleMock(configApiRef, () => ({
|
||||
has: jest.fn(),
|
||||
keys: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getOptional: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
getOptionalConfig: jest.fn(),
|
||||
getConfigArray: jest.fn(),
|
||||
getOptionalConfigArray: jest.fn(),
|
||||
getNumber: jest.fn(),
|
||||
getOptionalNumber: jest.fn(),
|
||||
getBoolean: jest.fn(),
|
||||
getOptionalBoolean: jest.fn(),
|
||||
getString: jest.fn(),
|
||||
getOptionalString: jest.fn(),
|
||||
getStringArray: jest.fn(),
|
||||
getOptionalStringArray: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#DiscoveryApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function discovery(options?: {
|
||||
baseUrl?: string;
|
||||
}): DiscoveryApi & MockWithApiFactory<DiscoveryApi> {
|
||||
const baseUrl = options?.baseUrl ?? 'http://example.com';
|
||||
const instance: DiscoveryApi = {
|
||||
async getBaseUrl(pluginId: string) {
|
||||
return `${baseUrl}/api/${pluginId}`;
|
||||
},
|
||||
};
|
||||
return mockWithApiFactory(discoveryApiRef, instance) as DiscoveryApi &
|
||||
MockWithApiFactory<DiscoveryApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#DiscoveryApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace discovery {
|
||||
export const mock = simpleMock(discoveryApiRef, () => ({
|
||||
getBaseUrl: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#IdentityApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function identity(options?: {
|
||||
userEntityRef?: string;
|
||||
ownershipEntityRefs?: string[];
|
||||
token?: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
picture?: string;
|
||||
}): IdentityApi & MockWithApiFactory<IdentityApi> {
|
||||
const {
|
||||
userEntityRef = 'user:default/test',
|
||||
ownershipEntityRefs = ['user:default/test'],
|
||||
token,
|
||||
email,
|
||||
displayName,
|
||||
picture,
|
||||
} = options ?? {};
|
||||
const instance: IdentityApi = {
|
||||
async getBackstageIdentity() {
|
||||
return { type: 'user', ownershipEntityRefs, userEntityRef };
|
||||
},
|
||||
async getCredentials() {
|
||||
return { token };
|
||||
},
|
||||
async getProfileInfo() {
|
||||
return { email, displayName, picture };
|
||||
},
|
||||
async signOut() {},
|
||||
};
|
||||
return mockWithApiFactory(identityApiRef, instance) as IdentityApi &
|
||||
MockWithApiFactory<IdentityApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#IdentityApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace identity {
|
||||
export const mock = simpleMock(identityApiRef, () => ({
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/plugin-permission-react#PermissionApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function permission(options?: {
|
||||
authorize?:
|
||||
| AuthorizeResult.ALLOW
|
||||
| AuthorizeResult.DENY
|
||||
| ((
|
||||
request: EvaluatePermissionRequest,
|
||||
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY);
|
||||
}): MockPermissionApi & MockWithApiFactory<PermissionApi> {
|
||||
const authorizeInput = options?.authorize;
|
||||
const handler =
|
||||
typeof authorizeInput === 'function'
|
||||
? authorizeInput
|
||||
: () => authorizeInput ?? AuthorizeResult.ALLOW;
|
||||
const instance = new MockPermissionApi(handler);
|
||||
return mockWithApiFactory(permissionApiRef, instance) as MockPermissionApi &
|
||||
MockWithApiFactory<PermissionApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/plugin-permission-react#PermissionApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace permission {
|
||||
export const mock = simpleMock(permissionApiRef, () => ({
|
||||
authorize: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#StorageApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function storage(options?: {
|
||||
data?: JsonObject;
|
||||
}): MockStorageApi & MockWithApiFactory<StorageApi> {
|
||||
const instance = MockStorageApi.create(options?.data);
|
||||
return mockWithApiFactory(storageApiRef, instance) as MockStorageApi &
|
||||
MockWithApiFactory<StorageApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#StorageApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace storage {
|
||||
export const mock = simpleMock(storageApiRef, () => ({
|
||||
forBucket: jest.fn(),
|
||||
snapshot: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
observe$: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#ErrorApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function error(
|
||||
options?: MockErrorApiOptions,
|
||||
): MockErrorApi & MockWithApiFactory<ErrorApi> {
|
||||
const instance = new MockErrorApi(options);
|
||||
return mockWithApiFactory(errorApiRef, instance) as MockErrorApi &
|
||||
MockWithApiFactory<ErrorApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#ErrorApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace error {
|
||||
export const mock = simpleMock(errorApiRef, () => ({
|
||||
post: jest.fn(),
|
||||
error$: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of {@link @backstage/core-plugin-api#FetchApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function fetch(
|
||||
options?: MockFetchApiOptions,
|
||||
): MockFetchApi & MockWithApiFactory<FetchApi> {
|
||||
const instance = new MockFetchApi(options);
|
||||
return mockWithApiFactory(fetchApiRef, instance) as MockFetchApi &
|
||||
MockWithApiFactory<FetchApi>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock helpers for {@link @backstage/core-plugin-api#FetchApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace fetch {
|
||||
export const mock = simpleMock(fetchApiRef, () => ({
|
||||
fetch: jest.fn(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,13 @@ export type MockWithApiFactory<TApi> = TApi & {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function mockWithApiFactory<TApi>(
|
||||
export function mockWithApiFactory<TApi, TImpl extends TApi = TApi>(
|
||||
apiRef: ApiRef<TApi>,
|
||||
implementation: TApi,
|
||||
): MockWithApiFactory<TApi> {
|
||||
const marked = implementation as MockWithApiFactory<TApi>;
|
||||
implementation: TImpl,
|
||||
): TImpl & { [mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}> } {
|
||||
const marked = implementation as TImpl & {
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
};
|
||||
(marked as any)[mockApiFactorySymbol] = {
|
||||
api: apiRef,
|
||||
deps: {},
|
||||
|
||||
@@ -3952,16 +3952,23 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/frontend-app-api": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-app": "workspace:^"
|
||||
"@backstage/plugin-app-react": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@testing-library/jest-dom": "npm:^6.0.0"
|
||||
"@types/react": "npm:^18.0.0"
|
||||
"@types/zen-observable": "npm:^0.8.0"
|
||||
cross-fetch: "npm:^4.0.0"
|
||||
i18next: "npm:^22.4.15"
|
||||
msw: "npm:^2.0.0"
|
||||
react: "npm:^18.0.2"
|
||||
react-dom: "npm:^18.0.2"
|
||||
react-router-dom: "npm:^6.30.2"
|
||||
|
||||
Reference in New Issue
Block a user