diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 9ee4fe3165..20cc35f423 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,29 +18,16 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CatalogListResponse, DiscoveryApi, IdentityApi } from './types'; +import { CatalogListResponse, DiscoveryApi } from './types'; const server = setupServer(); +const token = 'fake-token'; const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discoveryApi: DiscoveryApi = { async getBaseUrl(_pluginId) { return mockBaseUrl; }, }; -const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, - async getIdToken() { - return Promise.resolve('fake-id-token'); - }, - async signOut() { - return Promise.resolve(); - }, -}; describe('CatalogClient', () => { let client: CatalogClient; @@ -50,7 +37,7 @@ describe('CatalogClient', () => { afterEach(() => server.resetHandlers()); beforeEach(() => { - client = new CatalogClient({ discoveryApi, identityApi }); + client = new CatalogClient({ discoveryApi }); }); describe('getEntities', () => { @@ -85,7 +72,7 @@ describe('CatalogClient', () => { }); it('should entities from correct endpoint', async () => { - const response = await client.getEntities(); + const response = await client.getEntities(token); expect(response).toEqual(defaultResponse); }); @@ -99,7 +86,7 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ + const response = await client.getEntities(token, { filter: { a: '1', b: ['2', '3'], @@ -120,7 +107,7 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ + const response = await client.getEntities(token, { fields: ['a.b', 'รถ'], }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index c559cb1210..c4f8c3d2b0 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -24,30 +24,27 @@ import fetch from 'cross-fetch'; import { AddLocationRequest, AddLocationResponse, - CatalogApi, CatalogEntitiesRequest, CatalogListResponse, DiscoveryApi, - IdentityApi, } from './types'; -export class CatalogClient implements CatalogApi { +export class CatalogClient { private readonly discoveryApi: DiscoveryApi; - private readonly identityApi: IdentityApi; - constructor(options: { - discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - }) { + constructor(options: { discoveryApi: DiscoveryApi }) { this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; } - async getLocationById(id: String): Promise { - return await this.getOptional(`/locations/${id}`); + async getLocationById( + token: string | undefined, + id: String, + ): Promise { + return await this.getOptional(token, `/locations/${id}`); } async getEntities( + token: string | undefined, request?: CatalogEntitiesRequest, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; @@ -68,28 +65,35 @@ export class CatalogClient implements CatalogApi { } const query = params.length ? `?${params.join('&')}` : ''; - const entities: Entity[] = await this.getRequired(`/entities${query}`); + const entities: Entity[] = await this.getRequired( + token, + `/entities${query}`, + ); return { items: entities }; } - async getEntityByName(compoundName: EntityName): Promise { + async getEntityByName( + token: string | undefined, + compoundName: EntityName, + ): Promise { const { kind, namespace = 'default', name } = compoundName; - return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`); + return this.getOptional( + token, + `/entities/by-name/${kind}/${namespace}/${name}`, + ); } - async addLocation({ - type = 'url', - target, - dryRun, - }: AddLocationRequest): Promise { - const idToken = await this.identityApi.getIdToken(); + async addLocation( + token: string | undefined, + { type = 'url', target, dryRun }: AddLocationRequest, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, { headers: { - authorization: `Bearer ${idToken}`, + authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, method: 'POST', @@ -118,21 +122,29 @@ export class CatalogClient implements CatalogApi { }; } - async getLocationByEntity(entity: Entity): Promise { + async getLocationByEntity( + token: string | undefined, + entity: Entity, + ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - const all: { data: Location }[] = await this.getRequired('/locations'); + const all: { data: Location }[] = await this.getRequired( + token, + '/locations', + ); return all .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } - async removeEntityByUid(uid: string): Promise { - const idToken = await this.identityApi.getIdToken(); + async removeEntityByUid( + token: string | undefined, + uid: string, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { headers: { - authorization: `Bearer ${idToken}`, + authorization: `Bearer ${token}`, }, method: 'DELETE', }, @@ -150,13 +162,13 @@ export class CatalogClient implements CatalogApi { // Private methods // - private async getRequired(path: string): Promise { + private async getRequired( + token: string | undefined, + path: string, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { - headers: { - authorization: `Bearer ${idToken}`, - }, + headers: token ? { authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { @@ -168,13 +180,13 @@ export class CatalogClient implements CatalogApi { return await response.json(); } - private async getOptional(path: string): Promise { + private async getOptional( + token: string | undefined, + path: string, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { - headers: { - authorization: `Bearer ${idToken}`, - }, + headers: token ? { authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { diff --git a/packages/catalog-client/src/index.ts b/packages/catalog-client/src/index.ts index 9bd13f9b7c..c5a626e25b 100644 --- a/packages/catalog-client/src/index.ts +++ b/packages/catalog-client/src/index.ts @@ -15,4 +15,10 @@ */ export { CatalogClient } from './CatalogClient'; -export type { CatalogApi } from './types'; +export type { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, +} from './types'; diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index 0742dfed53..e72317d444 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -53,17 +53,3 @@ export type AddLocationResponse = { export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; -/** - * This is a copy of the core IdentityApi, to avoid importing core. - */ -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; -export type IdentityApi = { - getUserId(): string; - getProfile(): ProfileInfo; - getIdToken(): Promise; - signOut(): Promise; -}; diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts new file mode 100644 index 0000000000..8a5c58194a --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CatalogClient } from '@backstage/catalog-client'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; + +jest.mock('./CatalogClient'); +const MockedCatalogClient = CatalogClient as jest.Mock; + +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi: DiscoveryApi = { + async getBaseUrl(_pluginId) { + return mockBaseUrl; + }, +}; +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; + +describe('CatalogClientWrapper', () => { + let client: CatalogClientWrapper; + + beforeEach(() => { + MockedCatalogClient.mockClear(); + client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + }); + + describe('getEntities', () => { + it('injects authorization token', async () => { + expect.assertions(2); + await client.getEntities(); + const getEntities = MockedCatalogClient.mock.instances[0].getEntities; + expect(getEntities).toHaveBeenCalledWith('fake-id-token', undefined); + expect(getEntities).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts new file mode 100644 index 0000000000..fab551de48 --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, + CatalogClient, +} from '@backstage/catalog-client'; +import { IdentityApi } from '@backstage/core'; + +export class CatalogClientWrapper implements CatalogApi { + private readonly identityApi: IdentityApi; + private readonly client: CatalogClient; + + constructor(options: { client: CatalogClient; identityApi: IdentityApi }) { + this.client = options.client; + this.identityApi = options.identityApi; + } + + async getLocationById(id: String): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getLocationById(token, id); + } + + async getEntities( + request?: CatalogEntitiesRequest, + ): Promise> { + const token = await this.identityApi.getIdToken(); + return await this.client.getEntities(token, request); + } + + async getEntityByName(compoundName: EntityName): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getEntityByName(token, compoundName); + } + + async addLocation(request: AddLocationRequest): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.addLocation(token, request); + } + + async getLocationByEntity(entity: Entity): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getLocationByEntity(token, entity); + } + + async removeEntityByUid(uid: string): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.removeEntityByUid(token, uid); + } +} diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 557762ee46..55a9f29372 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -27,6 +27,8 @@ import { catalogRouteRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { catalogRouteRef, entityRouteRef } from './routes'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; export const catalogPlugin = createPlugin({ id: 'catalog', @@ -35,7 +37,10 @@ export const catalogPlugin = createPlugin({ api: catalogApiRef, deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, factory: ({ discoveryApi, identityApi }) => - new CatalogClient({ discoveryApi, identityApi }), + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), }), ], routes: {