implement identity too

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-10-08 22:49:42 +02:00
parent 811ff0cddc
commit b52715bc2e
27 changed files with 396 additions and 288 deletions
@@ -15,8 +15,8 @@
*/
import { ConfigReader } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
import { mockApis } from '@backstage/test-utils';
describe('IdentityAuthInjectorFetchMiddleware', () => {
it('creates using defaults', async () => {
@@ -58,10 +58,7 @@ describe('IdentityAuthInjectorFetchMiddleware', () => {
});
it('injects the header only when a token is available', async () => {
const tokenFunction = jest.fn();
const identityApi = {
getCredentials: tokenFunction,
} as unknown as IdentityApi;
const identityApi = mockApis.identity();
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
@@ -73,27 +70,25 @@ describe('IdentityAuthInjectorFetchMiddleware', () => {
const outer = middleware.apply(inner);
// No token available
tokenFunction.mockResolvedValueOnce({ token: undefined });
identityApi.getCredentials.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
// Supply a token, header gets added
tokenFunction.mockResolvedValueOnce({ token: 'token' });
identityApi.getCredentials.mockResolvedValueOnce({ token: 'token' });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Token no longer available
tokenFunction.mockResolvedValueOnce({ token: undefined });
identityApi.getCredentials.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
});
it('does not overwrite an existing header with the same name', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const identityApi = mockApis.identity({ token: 'token' });
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
@@ -18,7 +18,6 @@ import React from 'react';
import {
AppComponents,
configApiRef,
IdentityApi,
identityApiRef,
SignInPageProps,
useApi,
@@ -30,7 +29,7 @@ import { render, screen } from '@testing-library/react';
import { AppRouter } from './AppRouter';
import useAsync from 'react-use/esm/useAsync';
import { AppContextProvider } from './AppContext';
import { TestApiProvider } from '@backstage/test-utils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { ConfigReader } from '@backstage/config';
function UserRefDisplay() {
@@ -78,13 +77,7 @@ describe('AppRouter', () => {
const appIdentityProxy = new AppIdentityProxy();
const SignInPage = (props: SignInPageProps) => {
props.onSignInSuccess({
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/test',
ownershipEntityRefs: ['user:default/test'],
}),
} as IdentityApi);
props.onSignInSuccess(mockApis.identity());
return null;
};
@@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createMocks } from 'react-idle-timer';
// eslint-disable-next-line no-restricted-imports
import { MessageChannel } from 'worker_threads';
import { ApiProvider } from '@backstage/core-app-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils';
import {
TestApiRegistry,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import React from 'react';
import { AutoLogout } from './AutoLogout';
import { cleanup } from '@testing-library/react';
// Mock the signOut function of identityApiRef
const mockSignOut = jest.fn();
const mockIdentityApi = {
signOut: mockSignOut,
getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }),
};
const mockIdentityApi = mockApis.identity({ token: 'xxx' });
const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]);
describe('AutoLogout', () => {
@@ -15,7 +15,6 @@
*/
import {
IdentityApi,
SignInPageBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
@@ -70,9 +69,9 @@ describe('createPublicSignInApp', () => {
async () =>
({ onSignInSuccess }) => {
useEffect(() => {
onSignInSuccess({
getCredentials: async () => ({ token: 'mock-token' }),
} as IdentityApi);
onSignInSuccess(
mockApis.identity({ token: 'mock-token' }),
);
}, [onSignInSuccess]);
return <div />;
},
+36 -1
View File
@@ -100,7 +100,9 @@ export namespace mockApis {
const // (undocumented)
factory: () => ApiFactory<AnalyticsApi, AnalyticsApi, {}>;
const // (undocumented)
mock: () => jest.Mocked<AnalyticsApi>;
mock: (
partialImpl?: Partial<AnalyticsApi> | undefined,
) => ApiMock<AnalyticsApi>;
}
export function config(options?: { data?: JsonObject }): ConfigApi;
export namespace config {
@@ -113,6 +115,35 @@ export namespace mockApis {
) => ApiFactory<Config, Config, {}>;
const mock: (partialImpl?: Partial<Config> | undefined) => ApiMock<Config>;
}
// (undocumented)
export function identity(options?: {
userEntityRef?: string;
ownershipEntityRefs?: string[];
token?: string;
email?: string;
displayName?: string;
picture?: string;
}): jest.Mocked<IdentityApi>;
// (undocumented)
export namespace identity {
const // (undocumented)
factory: (
options?:
| {
userEntityRef?: string | undefined;
ownershipEntityRefs?: string[] | undefined;
token?: string | undefined;
email?: string | undefined;
displayName?: string | undefined;
picture?: string | undefined;
}
| undefined,
) => ApiFactory<IdentityApi, IdentityApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<IdentityApi> | undefined,
) => ApiMock<IdentityApi>;
}
}
// @public @deprecated
@@ -331,4 +362,8 @@ export function wrapInTestApp(
// src/testUtils/apis/mockApis.d.ts:45:5 - (ae-undocumented) Missing documentation for "analytics".
// src/testUtils/apis/mockApis.d.ts:46:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:47:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:98:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:106:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:107:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:115:15 - (ae-undocumented) Missing documentation for "mock".
```
@@ -17,13 +17,49 @@
import { mockApis } from './mockApis';
describe('mockApis', () => {
describe('analytics', () => {
it('can create an instance and make assertions on it', () => {
const analytics = mockApis.analytics();
expect(
analytics.captureEvent({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
}),
).toBeUndefined();
expect(analytics.captureEvent).toHaveBeenCalledTimes(1);
});
it('can create a mock and make assertions on it', async () => {
expect.assertions(3);
const analytics = mockApis.analytics.mock({
captureEvent: event => {
expect(event).toEqual({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
});
},
});
expect(
analytics.captureEvent({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
}),
).toBeUndefined();
expect(analytics.captureEvent).toHaveBeenCalledTimes(1);
});
});
describe('config', () => {
const data = { backend: { baseUrl: 'http://test.com' } };
it('can create an instance', () => {
const empty = mockApis.config();
const notEmpty = mockApis.config({ data });
expect(empty.getOptional('backend.baseUrl')).toBeUndefined();
const notEmpty = mockApis.config({ data });
expect(notEmpty.getOptional('backend.baseUrl')).toEqual(
'http://test.com',
);
@@ -35,4 +71,90 @@ describe('mockApis', () => {
expect(mock.getString).toHaveBeenCalledTimes(1);
});
});
describe('identity', () => {
it('can create an instance and make assertions on it', async () => {
const empty = mockApis.identity();
await expect(empty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'user:default/test',
ownershipEntityRefs: ['user:default/test'],
});
await expect(empty.getCredentials()).resolves.toEqual({});
await expect(empty.getProfileInfo()).resolves.toEqual({});
await expect(empty.signOut()).resolves.toBeUndefined();
expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(empty.getCredentials).toHaveBeenCalledTimes(1);
expect(empty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(empty.signOut).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.identity({
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
token: 'c',
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
});
await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' });
await expect(notEmpty.getProfileInfo()).resolves.toEqual({
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.signOut()).resolves.toBeUndefined();
expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1);
expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(notEmpty.signOut).toHaveBeenCalledTimes(1);
});
it('can create a mock and make assertions on it', async () => {
const empty = mockApis.identity.mock();
expect(empty.getBackstageIdentity()).toBeUndefined();
expect(empty.getCredentials()).toBeUndefined();
expect(empty.getProfileInfo()).toBeUndefined();
expect(empty.signOut()).toBeUndefined();
expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(empty.getCredentials).toHaveBeenCalledTimes(1);
expect(empty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(empty.signOut).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.identity.mock({
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
}),
getCredentials: async () => ({ token: 'c' }),
getProfileInfo: async () => ({
email: 'd',
displayName: 'e',
picture: 'f',
}),
signOut: async () => undefined,
});
await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
});
await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' });
await expect(notEmpty.getProfileInfo()).resolves.toEqual({
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.signOut()).resolves.toBeUndefined();
expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1);
expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(notEmpty.signOut).toHaveBeenCalledTimes(1);
});
});
});
@@ -20,9 +20,11 @@ import {
ApiFactory,
ApiRef,
ConfigApi,
IdentityApi,
analyticsApiRef,
configApiRef,
createApiFactory,
identityApiRef,
} from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { ApiMock } from './ApiMock';
@@ -113,7 +115,7 @@ export namespace mockApis {
}
export namespace analytics {
export const factory = simpleFactory(analyticsApiRef, analytics);
export const mock = analyticsMockSkeleton;
export const mock = simpleMock(analyticsApiRef, analyticsMockSkeleton);
}
/**
@@ -180,4 +182,48 @@ export namespace mockApis {
getOptionalStringArray: jest.fn(),
}));
}
const identityMockSkeleton = (): jest.Mocked<IdentityApi> => ({
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getProfileInfo: jest.fn(),
signOut: jest.fn(),
});
export function identity(options?: {
userEntityRef?: string;
ownershipEntityRefs?: string[];
token?: string;
email?: string;
displayName?: string;
picture?: string;
}) {
const {
userEntityRef = 'user:default/test',
ownershipEntityRefs = ['user:default/test'],
token,
email,
displayName,
picture,
} = options ?? {};
return simpleInstance(
identityApiRef,
{
async getBackstageIdentity() {
return { type: 'user', ownershipEntityRefs, userEntityRef };
},
async getCredentials() {
return { token };
},
async getProfileInfo() {
return { email, displayName, picture };
},
async signOut() {},
},
identityMockSkeleton,
);
}
export namespace identity {
export const factory = simpleFactory(identityApiRef, identity);
export const mock = simpleMock(identityApiRef, identityMockSkeleton);
}
}
@@ -16,12 +16,16 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import {
TestApiProvider,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import { identityApiRef } from '@backstage/core-plugin-api';
import { CookieAuthRedirect } from './CookieAuthRedirect';
describe('CookieAuthRedirect', () => {
const identityApiMock = { getCredentials: jest.fn() };
const identityApiMock = mockApis.identity.mock();
beforeEach(() => {
jest.clearAllMocks();
@@ -36,13 +36,12 @@ import { catalogApiRef } from '../../api';
import {
MockStorageApi,
TestApiRegistry,
mockApis,
renderInTestApp,
} from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
import {
ConfigApi,
configApiRef,
IdentityApi,
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
@@ -61,15 +60,18 @@ const mockUser: UserEntity = {
},
};
const mockConfigApi = {
getOptionalString: () => 'Test Company',
} as Partial<ConfigApi>;
const ownershipEntityRefs = ['user:default/testuser'];
const mockConfigApi = mockApis.config({
data: { organization: { name: 'Test Company' } },
});
const mockCatalogApi = catalogApiMock.mock();
const mockIdentityApi = {
getBackstageIdentity: jest.fn(),
} as Partial<jest.Mocked<IdentityApi>>;
const mockIdentityApi = mockApis.identity({
userEntityRef: ownershipEntityRefs[0],
ownershipEntityRefs,
});
const mockStarredEntitiesApi = new MockStarredEntitiesApi();
@@ -81,7 +83,6 @@ const apis = TestApiRegistry.from(
[starredEntitiesApiRef, mockStarredEntitiesApi],
);
const ownershipEntityRefs = ['user:default/testuser'];
describe('<UserListPicker />', () => {
const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] =
async request => {
@@ -23,11 +23,7 @@ import {
useEntityList,
} from '../../hooks';
import { catalogApiRef } from '../../api';
import {
ApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { ApiRef, identityApiRef } from '@backstage/core-plugin-api';
import { MemoryRouter } from 'react-router-dom';
import { useOwnedEntitiesCount } from './useOwnedEntitiesCount';
import {
@@ -36,12 +32,13 @@ import {
EntityUserFilter,
} from '../../filters';
import { useMountEffect } from '@react-hookz/web';
import { mockApis } from '@backstage/test-utils';
const mockCatalogApi = catalogApiMock.mock();
const mockGetBackstageIdentity: jest.MockedFn<
IdentityApi['getBackstageIdentity']
> = jest.fn();
const mockIdentityApi = mockApis.identity({
ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'],
userEntityRef: 'user:default/spiderman',
});
jest.mock('@backstage/core-plugin-api', () => {
const actual = jest.requireActual('@backstage/core-plugin-api');
@@ -50,13 +47,9 @@ jest.mock('@backstage/core-plugin-api', () => {
useApi: (ref: ApiRef<any>) => {
if (ref === catalogApiRef) {
return mockCatalogApi;
} else if (ref === identityApiRef) {
return mockIdentityApi;
}
if (ref === identityApiRef) {
return {
getBackstageIdentity: mockGetBackstageIdentity,
};
}
return actual.useApi(ref);
},
};
@@ -65,12 +58,6 @@ jest.mock('@backstage/core-plugin-api', () => {
describe('useOwnedEntitiesCount', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetBackstageIdentity.mockResolvedValue({
ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'],
userEntityRef: 'user:default/spiderman',
type: 'user',
});
});
it(`shouldn't invoke queryEntities when filters are loading`, async () => {
@@ -84,7 +71,9 @@ describe('useOwnedEntitiesCount', () => {
wrapper: createWrapperWithInitialFilters({}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await expect(
waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()),
@@ -114,7 +103,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
@@ -151,7 +142,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await expect(
waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()),
@@ -185,7 +178,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
@@ -18,14 +18,16 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { Entity } from '@backstage/catalog-model';
import {
alertApiRef,
ConfigApi,
configApiRef,
errorApiRef,
IdentityApi,
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { MockStorageApi, TestApiProvider } from '@backstage/test-utils';
import {
MockStorageApi,
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import React, { PropsWithChildren } from 'react';
@@ -67,20 +69,14 @@ const entities: Entity[] = [
},
];
const mockConfigApi = {
getOptionalString: () => '',
} as Partial<ConfigApi>;
const mockConfigApi = mockApis.config();
const ownershipEntityRefs = ['user:default/guest'];
const mockIdentityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs,
}),
getCredentials: async () => ({ token: undefined }),
};
const mockIdentityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs,
});
const mockCatalogApi = catalogApiMock.mock({
getEntities: jest.fn().mockResolvedValue({ items: entities }),
queryEntities: jest.fn().mockResolvedValue({
@@ -15,20 +15,17 @@
*/
import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider } from '@backstage/test-utils';
import { identityApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { useEntityOwnership } from './useEntityOwnership';
describe('useEntityOwnership', () => {
type MockIdentityApi = jest.Mocked<Pick<IdentityApi, 'getBackstageIdentity'>>;
const mockIdentityApi: MockIdentityApi = {
getBackstageIdentity: jest.fn(),
};
const identityApi = mockIdentityApi as unknown as IdentityApi;
const identityApi = mockApis.identity({
userEntityRef: 'user:default/user1',
ownershipEntityRefs: ['user:default/user1', 'group:default/group1'],
});
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
@@ -64,12 +61,6 @@ describe('useEntityOwnership', () => {
describe('useEntityOwnership', () => {
it('matches ownership via ownership entity refs', async () => {
mockIdentityApi.getBackstageIdentity.mockResolvedValue({
type: 'user',
userEntityRef: 'user:default/user1',
ownershipEntityRefs: ['user:default/user1', 'group:default/group1'],
});
const { result } = renderHook(() => useEntityOwnership(), {
wrapper: Wrapper,
});
@@ -17,12 +17,7 @@
import { QueryEntitiesInitialRequest } from '@backstage/catalog-client';
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { TableColumn, TableProps } from '@backstage/core-components';
import {
IdentityApi,
identityApiRef,
ProfileInfo,
storageApiRef,
} from '@backstage/core-plugin-api';
import { identityApiRef, storageApiRef } from '@backstage/core-plugin-api';
import {
catalogApiRef,
entityRouteRef,
@@ -34,6 +29,7 @@ import {
MockPermissionApi,
MockStorageApi,
TestApiProvider,
mockApis,
renderInTestApp,
} from '@backstage/test-utils';
import DashboardIcon from '@material-ui/icons/Dashboard';
@@ -166,18 +162,11 @@ describe('DefaultCatalogPage', () => {
}),
});
const testProfile: Partial<ProfileInfo> = {
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
}),
getCredentials: async () => ({ token: undefined }),
getProfileInfo: async () => testProfile,
};
});
const storageApi = MockStorageApi.create();
const renderWrapped = (children: React.ReactNode) =>
+5 -10
View File
@@ -14,9 +14,8 @@
* limitations under the License.
*/
import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api';
import { VisitsStorageApi } from './VisitsStorageApi';
import { MockStorageApi } from '@backstage/test-utils';
import { MockStorageApi, mockApis } from '@backstage/test-utils';
import { Visit, VisitsApi } from './VisitsApi';
describe('VisitsStorageApi.create', () => {
@@ -26,13 +25,9 @@ describe('VisitsStorageApi.create', () => {
() => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf
) as `${string}-${string}-${string}-${string}-${string}`;
const mockIdentityApi: IdentityApi = {
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: async () =>
({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity),
getCredentials: jest.fn(),
};
const mockIdentityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
});
beforeEach(() => {
window.crypto.randomUUID = mockRandomUUID;
@@ -40,7 +35,7 @@ describe('VisitsStorageApi.create', () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.useRealTimers();
window.localStorage.clear();
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api';
import { mockApis } from '@backstage/test-utils';
import { VisitsWebStorageApi } from './VisitsWebStorageApi';
describe('VisitsWebStorageApi.create()', () => {
@@ -24,13 +24,9 @@ describe('VisitsWebStorageApi.create()', () => {
() => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf
) as `${string}-${string}-${string}-${string}-${string}`;
const mockIdentityApi: IdentityApi = {
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: async () =>
({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity),
getCredentials: jest.fn(),
};
const mockIdentityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
});
const mockErrorApi = { post: jest.fn(), error$: jest.fn() };
@@ -40,7 +36,7 @@ describe('VisitsWebStorageApi.create()', () => {
afterEach(() => {
window.localStorage.clear();
jest.resetAllMocks();
jest.clearAllMocks();
});
it('instantiates with only identitiyApi', () => {
@@ -14,11 +14,15 @@
* limitations under the License.
*/
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import React from 'react';
import { MyGroupsSidebarItem } from './MyGroupsSidebarItem';
import GroupIcon from '@material-ui/icons/People';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
@@ -26,13 +30,10 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
describe('MyGroupsSidebarItem Test', () => {
describe('For guests or users with no groups', () => {
it('MyGroupsSidebarItem should be empty', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
});
const catalogApi = catalogApiMock();
const rendered = await renderInTestApp(
<TestApiProvider
@@ -59,13 +60,10 @@ describe('MyGroupsSidebarItem Test', () => {
describe('For users that are members of a single group', () => {
it('MyGroupsSidebarItem should display a single item that links to their group', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
}),
};
const identityApi = mockApis.identity({
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
});
const catalogApi = catalogApiMock.mock({
getEntities: async () => ({
items: [
@@ -114,13 +112,10 @@ describe('MyGroupsSidebarItem Test', () => {
describe('For users that are members of multiple groups', () => {
it('MyGroupsSidebarItem should display a sub-menu with all their groups and a link to each group', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
}),
};
const identityApi = mockApis.identity({
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
});
const catalogApi = catalogApiMock.mock({
getEntities: async () => ({
items: [
@@ -192,13 +187,10 @@ describe('MyGroupsSidebarItem Test', () => {
describe('When an additional filter is not provided', () => {
it('catalogApi.getEntities() should be called with the default filter', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
});
const mockCatalogApi = catalogApiMock.mock();
await renderInTestApp(
<TestApiProvider
@@ -233,13 +225,10 @@ describe('MyGroupsSidebarItem Test', () => {
describe('When an additional filter is provided', () => {
it('catalogApi.getEntities() should be called with an additional filter item', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
});
const mockCatalogApi = catalogApiMock.mock();
await renderInTestApp(
<TestApiProvider
@@ -15,7 +15,11 @@
*/
import { Entity } from '@backstage/catalog-model';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import React from 'react';
@@ -31,12 +35,7 @@ import { rootRouteRef } from '../../routes';
describe('<ListTasksPage />', () => {
const catalogApi = catalogApiMock.mock();
const identityApi = {
getBackstageIdentity: jest.fn(),
getProfileInfo: jest.fn(),
getCredentials: jest.fn(),
signOut: jest.fn(),
};
const identityApi = mockApis.identity();
const scaffolderApiMock: jest.Mocked<Required<ScaffolderApi>> = {
scaffold: jest.fn(),
@@ -15,7 +15,11 @@
*/
import { Entity } from '@backstage/catalog-model';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import React from 'react';
@@ -24,13 +28,7 @@ import { identityApiRef } from '@backstage/core-plugin-api';
describe('<OwnerEntityColumn />', () => {
const catalogApi = catalogApiMock.mock();
const identityApi = {
getBackstageIdentity: jest.fn(),
getProfileInfo: jest.fn(),
getCredentials: jest.fn(),
signOut: jest.fn(),
};
const identityApi = mockApis.identity();
it('should render the column with the user', async () => {
const props = {
@@ -18,7 +18,11 @@ import React from 'react';
import { waitFor } from '@testing-library/react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { MyGroupsPicker } from './MyGroupsPicker';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import {
catalogApiRef,
entityPresentationApiRef,
@@ -26,7 +30,6 @@ import {
import { Entity } from '@backstage/catalog-model';
import {
ErrorApi,
IdentityApi,
errorApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
@@ -34,25 +37,9 @@ import userEvent from '@testing-library/user-event';
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
// Create a mock IdentityApi
const mockIdentityApi: IdentityApi = {
getProfileInfo: () =>
Promise.resolve({
displayName: 'Bob',
email: 'bob@example.com',
picture: 'https://example.com/picture.jpg',
}),
getBackstageIdentity: () =>
Promise.resolve({
id: 'Bob',
idToken: 'token',
type: 'user',
userEntityRef: 'user:default/bob',
ownershipEntityRefs: ['group:default/group1', 'group:default/group2'],
}),
getCredentials: () => Promise.resolve({ token: 'token' }),
signOut: () => Promise.resolve(),
};
const mockIdentityApi = mockApis.identity({
userEntityRef: 'user:default/bob',
});
describe('<MyGroupsPicker />', () => {
let entities: Entity[];
@@ -96,7 +83,7 @@ describe('<MyGroupsPicker />', () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it('should only return the groups a user is part of and not the groups a user is not part of', async () => {
@@ -14,16 +14,14 @@
* limitations under the License.
*/
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { mockApis } from '@backstage/test-utils';
import WS from 'jest-websocket-mock';
import { SignalClient } from './SignalClient';
describe('SignalsClient', () => {
const tokenFunction = jest.fn();
const baseUrlFunction = jest.fn();
const identity = {
getCredentials: tokenFunction,
} as unknown as IdentityApi;
const identity = mockApis.identity({ token: '12345' });
const discoveryApi = {
getBaseUrl: baseUrlFunction,
} as unknown as DiscoveryApi;
@@ -31,8 +29,7 @@ describe('SignalsClient', () => {
let server: WS;
beforeEach(async () => {
jest.resetAllMocks();
tokenFunction.mockResolvedValue({ token: '12345' });
jest.clearAllMocks();
baseUrlFunction.mockResolvedValue('http://localhost:1234');
server = new WS('ws://localhost:1234', { jsonProtocol: true });
});
+1 -4
View File
@@ -15,7 +15,6 @@
*/
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { NotFoundError } from '@backstage/errors';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { mockApis, MockFetchApi } from '@backstage/test-utils';
@@ -36,9 +35,7 @@ describe('TechDocsStorageClient', () => {
const mockBaseUrl = 'http://backstage:9191/api/techdocs';
const configApi = mockApis.config();
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const identityApi: jest.Mocked<IdentityApi> = {
getCredentials: jest.fn(),
} as unknown as jest.Mocked<IdentityApi>;
const identityApi = mockApis.identity();
const fetchApi = new MockFetchApi({ injectIdentityAuth: { identityApi } });
beforeEach(() => {
@@ -18,10 +18,13 @@ import {
DiscoveryApi,
ErrorApi,
FetchApi,
IdentityApi,
StorageApi,
} from '@backstage/core-plugin-api';
import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils';
import {
MockFetchApi,
mockApis,
registerMswTestHooks,
} from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserSettingsStorage } from './UserSettingsStorage';
@@ -35,13 +38,8 @@ describe('Persistent Storage API', () => {
const mockDiscoveryApi = {
getBaseUrl: async () => mockBaseUrl,
};
const mockIdentityApi: Partial<IdentityApi> = {
getCredentials: async () => ({ token: 'a-token' }),
};
const mockIdentityApiFallback: Partial<IdentityApi> = {
// This API recreates the guest mode, where the WebStorage is used as fallback
getCredentials: async () => ({}),
};
const mockIdentityApi = mockApis.identity({ token: 'a-token' });
const mockIdentityApiFallback = mockApis.identity();
const createPersistentStorage = (
args?: Partial<{
@@ -55,7 +53,7 @@ describe('Persistent Storage API', () => {
errorApi: mockErrorApi,
fetchApi: new MockFetchApi(),
discoveryApi: mockDiscoveryApi,
identityApi: mockIdentityApi as IdentityApi,
identityApi: mockIdentityApi,
...args,
});
};
@@ -72,13 +70,13 @@ describe('Persistent Storage API', () => {
errorApi: mockErrorApi,
fetchApi: new MockFetchApi(),
discoveryApi: mockDiscoveryApi,
identityApi: mockIdentityApiFallback as IdentityApi,
identityApi: mockIdentityApiFallback,
...args,
});
};
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it('should return undefined for values which are unset', async () => {
@@ -20,16 +20,15 @@ import { DefaultSettingsPage } from './DefaultSettingsPage';
import { UserSettingsTab } from '../UserSettingsTab';
import { useOutlet } from 'react-router-dom';
import { SettingsLayout } from '../SettingsLayout';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useOutlet: jest.fn().mockReturnValue(undefined),
}));
const catalogApiMock: jest.Mocked<CatalogApi> = {
getEntityByRef: jest.fn(),
} as any;
const catalogApi = catalogApiMock();
describe('<DefaultSettingsPage />', () => {
beforeEach(() => {
@@ -38,7 +37,7 @@ describe('<DefaultSettingsPage />', () => {
it('should render the settings page with 3 tabs', async () => {
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<DefaultSettingsPage />
</TestApiProvider>,
);
@@ -54,7 +53,7 @@ describe('<DefaultSettingsPage />', () => {
</UserSettingsTab>
);
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<DefaultSettingsPage tabs={[advancedTabRoute]} />
</TestApiProvider>,
);
@@ -71,7 +70,7 @@ describe('<DefaultSettingsPage />', () => {
</SettingsLayout.Route>
);
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<DefaultSettingsPage tabs={[advancedTabRoute]} />
</TestApiProvider>,
);
@@ -14,32 +14,28 @@
* limitations under the License.
*/
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiRegistry,
mockApis,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { UserSettingsIdentityCard } from './UserSettingsIdentityCard';
import { ApiProvider } from '@backstage/core-app-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
const apiRegistry = TestApiRegistry.from(
[
identityApiRef,
{
getProfileInfo: jest.fn(async () => ({})),
getBackstageIdentity: jest.fn(async () => ({
type: 'user' as const,
userEntityRef: 'foo:bar/foobar',
ownershipEntityRefs: ['user:default/test-ownership'],
})),
},
],
[
catalogApiRef,
{
getEntityByRef: jest.fn(),
},
mockApis.identity({
userEntityRef: 'foo:bar/foobar',
ownershipEntityRefs: ['user:default/test-ownership'],
}),
],
[catalogApiRef, catalogApiMock.mock()],
);
describe('<UserSettingsIdentityCard />', () => {
@@ -18,6 +18,7 @@ import {
MockErrorApi,
TestApiProvider,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import { errorApiRef, identityApiRef } from '@backstage/core-plugin-api';
import { fireEvent, waitFor, screen } from '@testing-library/react';
@@ -35,9 +36,9 @@ describe('<UserSettingsMenu />', () => {
});
it('handles errors that occur when signing out', async () => {
const failingIdentityApi = {
signOut: jest.fn().mockRejectedValue(new Error('Logout error')),
};
const failingIdentityApi = mockApis.identity.mock({
signOut: () => Promise.reject(new Error('Logout error')),
});
const mockErrorApi = new MockErrorApi({ collect: true });
await renderInTestApp(
<TestApiProvider
@@ -14,35 +14,30 @@
* limitations under the License.
*/
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiRegistry,
mockApis,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { identityApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { ApiProvider } from '@backstage/core-app-api';
import { UserSettingsProfileCard } from './UserSettingsProfileCard';
const apiRegistry = TestApiRegistry.from(
[
identityApiRef,
{
getProfileInfo: jest.fn(async () => ({})),
getBackstageIdentity: jest.fn(async () => ({
type: 'user' as const,
userEntityRef: 'foo:bar/foobar',
ownershipEntityRefs: ['user:default/test-ownership'],
})),
},
],
[identityApiRef, mockApis.identity()],
[
catalogApiRef,
{
getEntityByRef: jest.fn(async () => {
return {
catalogApiMock({
entities: [
{
apiVersion: 'backstage.io/v1beta1',
kind: 'User',
metadata: {
name: 'Guest',
name: 'test',
annotations: {},
},
spec: {
@@ -50,9 +45,9 @@ const apiRegistry = TestApiRegistry.from(
picture: 'https://example.com/avatar.png',
},
},
};
}),
},
},
],
}),
],
);
@@ -22,20 +22,15 @@ import { useOutlet } from 'react-router-dom';
import { SettingsLayout } from '../SettingsLayout';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
CatalogApi,
catalogApiRef,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useOutlet: jest.fn().mockReturnValue(undefined),
}));
const catalogApiMock: jest.Mocked<CatalogApi> = {
getEntityByRef: jest.fn(),
} as any;
const catalogApi = catalogApiMock();
describe('<SettingsPage />', () => {
beforeEach(() => {
@@ -44,7 +39,7 @@ describe('<SettingsPage />', () => {
it('should render the default settings page with 3 tabs', async () => {
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<SettingsPage />
</TestApiProvider>,
{
@@ -64,7 +59,7 @@ describe('<SettingsPage />', () => {
);
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<SettingsPage />
</TestApiProvider>,
{
@@ -85,7 +80,7 @@ describe('<SettingsPage />', () => {
);
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<SettingsPage />
</TestApiProvider>,
{
@@ -115,7 +110,7 @@ describe('<SettingsPage />', () => {
);
(useOutlet as jest.Mock).mockReturnValue(customLayout);
const { container } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<SettingsPage />
</TestApiProvider>,
{