diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts index a5beb5b6ec..84585394e9 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -13,15 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { useGetEntities } from './useGetEntities'; import { CatalogApi } from '@backstage/catalog-client'; import { renderHook } from '@testing-library/react-hooks'; +import { getEntityRelations } from '@backstage/plugin-catalog-react'; -const catalogApiMock: Pick = { - getEntities: jest.fn(async () => { - return Promise.resolve({ items: [] }); - }), +const givenParentGroup = 'team.squad1'; +const givenLeafGroup = 'team.squad2'; +const givenUser = 'user.john'; +const givenParentGroupEntity = { + kind: 'Group', + metadata: { + name: givenParentGroup, + }, +} as Partial as Entity; +const givenLeafGroupEntity = { + kind: 'Group', + metadata: { + name: givenLeafGroup, + }, +} as Partial as Entity; +const givenUserEntity = { + kind: 'User', + metadata: { + name: givenUser, + }, +} as Partial as Entity; + +const catalogApiMock: Pick = { + getEntities: jest.fn(async () => Promise.resolve({ items: [] })), + getEntityByRef: jest.fn(async ({ name }: CompoundEntityRef) => + name === givenParentGroup ? givenParentGroupEntity : givenLeafGroupEntity, + ), }; jest.mock('@backstage/core-plugin-api', () => ({ @@ -29,37 +53,106 @@ jest.mock('@backstage/core-plugin-api', () => ({ })); jest.mock('@backstage/plugin-catalog-react', () => ({ catalogApiRef: {}, - getEntityRelations: jest.fn(() => []), + getEntityRelations: jest.fn(entity => { + if (entity?.metadata.name === givenParentGroup) { + return [ + { + kind: 'Group', + namespace: 'default', + name: givenLeafGroup, + } as CompoundEntityRef, + ]; + } else if (entity?.kind === 'User') { + return [ + { + kind: 'Group', + namespace: 'default', + name: givenLeafGroup, + } as CompoundEntityRef, + ]; + } + + return []; + }) as typeof getEntityRelations, })); describe('useGetEntities', () => { - describe('given aggregated relationsType', () => { - it('when entity is group should aggregate child ownership', async () => { - const givenSquad = 'team.squad1'; - const whenEntity = { - kind: 'Group', - metadata: { - name: givenSquad, - }, - } as Partial as Entity; + expect.extend({ + ownedBy: (actual, ...owners: string[]) => { + try { + expect(actual).toHaveBeenCalledWith( + expect.objectContaining({ + filter: expect.arrayContaining([ + expect.objectContaining({ + 'relations.ownedBy': owners, + }), + ]), + }), + ); + return { + message: () => '', + pass: true, + }; + } catch (e) { + return { + message: () => e.message, + pass: false, + }; + } + }, + }); + describe('given aggregated relationsType', () => { + const whenHookIsCalledWith = async (_entity: Entity) => { const hook = renderHook( ({ entity }) => useGetEntities(entity, 'aggregated'), { - initialProps: { entity: whenEntity }, + initialProps: { entity: _entity }, }, ); await hook.waitForNextUpdate(); - expect(catalogApiMock.getEntities).toHaveBeenCalledWith( - expect.objectContaining({ - filter: expect.arrayContaining([ - expect.objectContaining({ - 'relations.ownedBy': [`group:default/${givenSquad}`], - }), - ]), - }), + }; + + it('given group entity should aggregate child ownership', async () => { + await whenHookIsCalledWith(givenParentGroupEntity); + expect(catalogApiMock.getEntities).ownedBy( + `group:default/${givenParentGroup}`, + `group:default/${givenLeafGroup}`, + ); + }); + + it('given user entity should aggregate parent ownership and direct', async () => { + await whenHookIsCalledWith(givenUserEntity); + expect(catalogApiMock.getEntities).ownedBy( + `group:default/${givenLeafGroup}`, + `user:default/${givenUser}`, ); }); }); + + describe('given direct relationsType', () => { + const whenHookIsCalledWith = async (_entity: Entity) => { + const hook = renderHook( + ({ entity }) => useGetEntities(entity, 'direct'), + { + initialProps: { entity: _entity }, + }, + ); + + await hook.waitForNextUpdate(); + }; + + it('given group entity should return directly owned entities', async () => { + await whenHookIsCalledWith(givenLeafGroupEntity); + expect(catalogApiMock.getEntities).ownedBy( + `group:default/${givenLeafGroup}`, + ); + }); + + it('given user entity should return directly owned entities', async () => { + await whenHookIsCalledWith(givenUserEntity); + expect(catalogApiMock.getEntities).ownedBy(`user:default/${givenUser}`); + }); + }); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index b18bf082a8..13390b4233 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -58,17 +58,24 @@ const getQueryParams = ( }; const getMemberOfEntityRefs = (owner: Entity): string[] => { - const ownerGroups = getEntityRelations(owner, RELATION_MEMBER_OF, { + const parentGroups = getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'Group', }); - const ownerGroupsNames = ownerGroups.map(ownerGroup => + + const ownerGroupsNames = parentGroups.map(({ kind, namespace, name }) => stringifyEntityRef({ - kind: ownerGroup.kind, - namespace: ownerGroup.namespace, - name: ownerGroup.name, + kind, + namespace, + name, }), ); - return ownerGroupsNames; + + const { + kind, + metadata: { namespace, name }, + } = owner; + + return [...ownerGroupsNames, stringifyEntityRef({ kind, namespace, name })]; }; const isEntity = (entity: Entity | undefined): entity is Entity => @@ -122,19 +129,20 @@ const getOwners = async ( const isAggregated = relationsType === 'aggregated'; const isUserEntity = entity.kind === 'User'; - const owners = [stringifyEntityRef(entity)]; + const owners: string[] = []; if (isAggregated && isGroup) { const childEntityRefs = await getChildOwnershipEntityRefs( entity, catalogApi, ); + owners.push(stringifyEntityRef(entity)); owners.push.apply(owners, childEntityRefs); - } - - if (isAggregated && isUserEntity) { + } else if (isAggregated && isUserEntity) { const parentEntityRefs = getMemberOfEntityRefs(entity); owners.push.apply(owners, parentEntityRefs); + } else { + owners.push(stringifyEntityRef(entity)); } return owners; @@ -149,6 +157,27 @@ export type AnyRelationsType = | (typeof DefaultRelationType)[keyof typeof DefaultRelationType] | string; +const getOwnedEntitiesByOwners = ( + owners: string[], + kinds: string[], + catalogApi: CatalogApi, +) => + catalogApi.getEntities({ + filter: [ + { + kind: kinds, + 'relations.ownedBy': owners, + }, + ], + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'spec.type', + 'relations', + ], + }); + export function useGetEntities( entity: Entity, relationsType: AnyRelationsType, @@ -176,21 +205,11 @@ export function useGetEntities( } = useAsync(async () => { const owners = await getOwners(entity, relationsType, catalogApi); - const ownedEntitiesList = await catalogApi.getEntities({ - filter: [ - { - kind: kinds, - 'relations.ownedBy': owners, - }, - ], - fields: [ - 'kind', - 'metadata.name', - 'metadata.namespace', - 'spec.type', - 'relations', - ], - }); + const ownedEntitiesList = await getOwnedEntitiesByOwners( + owners, + kinds, + catalogApi, + ); const counts = ownedEntitiesList.items.reduce( (acc: EntityTypeProps[], ownedEntity) => { diff --git a/plugins/org/src/setupTests.ts b/plugins/org/src/setupTests.ts index 28a35d2b06..09cb508b22 100644 --- a/plugins/org/src/setupTests.ts +++ b/plugins/org/src/setupTests.ts @@ -14,3 +14,7 @@ * limitations under the License. */ import '@testing-library/jest-dom'; + +beforeEach(() => { + jest.clearAllMocks(); +});