From 09e17c8a65abf0d6f9cbc77a0496461263bdb155 Mon Sep 17 00:00:00 2001 From: Marat Dyatko Date: Thu, 9 Apr 2026 10:24:04 +0200 Subject: [PATCH 1/2] test(org): add queryParams coverage for OwnershipCard useGetEntities The existing tests never exercised `getQueryParams` because the mock catalog API always returned empty items. These new tests verify that the generated query parameters include correctly humanized owner entity refs, handle multiple owners, and group entities by kind and type. Made-with: Cursor Signed-off-by: Marat Dyatko Made-with: Cursor --- .../OwnershipCard/useGetEntities.test.ts | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts index b7331479ef..d7ee61b714 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -18,6 +18,7 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { useGetEntities } from './useGetEntities'; import { renderHook, waitFor } from '@testing-library/react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; +import qs from 'qs'; const givenParentGroup = 'team.squad1'; const givenLeafGroup = 'team.squad2'; @@ -45,8 +46,11 @@ const getEntityRelationsMock: jest.Mock< [Entity | undefined] > = jest.fn(); jest.mock('@backstage/plugin-catalog-react', () => { + const actual = jest.requireActual('@backstage/plugin-catalog-react'); return { catalogApiRef: {}, + entityPresentationSnapshot: actual.entityPresentationSnapshot, + humanizeEntityRef: actual.humanizeEntityRef, getEntityRelations: jest.fn(entity => { return getEntityRelationsMock(entity); }) as any, @@ -283,6 +287,143 @@ describe('useGetEntities', () => { }); }); + describe('queryParams generation', () => { + afterEach(() => { + getEntityRelationsMock.mockRestore(); + catalogApi.getEntities.mockRestore(); + }); + + it('should produce query params with humanized entity refs as owners', async () => { + getEntityRelationsMock.mockReturnValue([]); + catalogApi.getEntities.mockResolvedValueOnce({ + items: [ + { + kind: 'Component', + metadata: { name: 'my-service', namespace: 'default' }, + spec: { type: 'service' }, + } as Partial as Entity, + ], + }); + + const { result } = renderHook( + ({ entity }) => useGetEntities(entity, 'direct'), + { initialProps: { entity: givenLeafGroupEntity } }, + ); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.componentsWithCounters).toHaveLength(1); + const params = qs.parse( + result.current.componentsWithCounters![0].queryParams, + ); + expect(params).toEqual({ + filters: { + kind: 'component', + type: 'service', + owners: givenLeafGroup, + user: 'all', + }, + }); + }); + + it('should include multiple owners as an array in query params', async () => { + getEntityRelationsMock + .mockReturnValueOnce([createGroupRefFromName(givenLeafGroup)]) + .mockReturnValue([]); + + catalogApi.getEntitiesByRefs.mockResolvedValueOnce({ + items: [givenLeafGroupEntity], + }); + catalogApi.getEntities.mockResolvedValueOnce({ + items: [ + { + kind: 'API', + metadata: { name: 'my-api', namespace: 'default' }, + spec: { type: 'openapi' }, + } as Partial as Entity, + ], + }); + + const { result } = renderHook( + ({ entity }) => useGetEntities(entity, 'aggregated'), + { initialProps: { entity: givenParentGroupEntity } }, + ); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.componentsWithCounters).toHaveLength(1); + const params = qs.parse( + result.current.componentsWithCounters![0].queryParams, + ); + expect(params).toEqual({ + filters: { + kind: 'api', + type: 'openapi', + owners: expect.arrayContaining([givenLeafGroup, givenParentGroup]), + user: 'all', + }, + }); + }); + + it('should group entities by kind and type in query params', async () => { + getEntityRelationsMock.mockReturnValue([]); + catalogApi.getEntities.mockResolvedValueOnce({ + items: [ + { + kind: 'Component', + metadata: { name: 'svc-1', namespace: 'default' }, + spec: { type: 'service' }, + } as Partial as Entity, + { + kind: 'Component', + metadata: { name: 'svc-2', namespace: 'default' }, + spec: { type: 'service' }, + } as Partial as Entity, + { + kind: 'API', + metadata: { name: 'api-1', namespace: 'default' }, + spec: { type: 'openapi' }, + } as Partial as Entity, + ], + }); + + const { result } = renderHook( + ({ entity }) => useGetEntities(entity, 'direct'), + { initialProps: { entity: givenLeafGroupEntity } }, + ); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.componentsWithCounters).toHaveLength(2); + + const componentEntry = result.current.componentsWithCounters!.find( + c => c.kind === 'Component', + )!; + expect(componentEntry.counter).toBe(2); + + const componentParams = qs.parse(componentEntry.queryParams); + expect(componentParams).toEqual({ + filters: expect.objectContaining({ + kind: 'component', + type: 'service', + }), + }); + + const apiEntry = result.current.componentsWithCounters!.find( + c => c.kind === 'API', + )!; + expect(apiEntry.counter).toBe(1); + + const apiParams = qs.parse(apiEntry.queryParams); + expect(apiParams).toEqual({ + filters: expect.objectContaining({ + kind: 'api', + type: 'openapi', + }), + }); + }); + }); + describe('useGetEntities exceeding default 16384 bytes header size', () => { const largeNumberOfGroups = Array.from({ length: 600 }, (_, i) => createGroupEntityFromName(`very-long-group-name-${i + 1}`), From f1f59b13e1cc1711b5790f02af106e128dd39417 Mon Sep 17 00:00:00 2001 From: Marat Dyatko Date: Thu, 9 Apr 2026 10:40:21 +0200 Subject: [PATCH 2/2] fix(org): replace deprecated humanizeEntityRef with Catalog Presentation API Replace `humanizeEntityRef(parseEntityRef(owner), { defaultKind: 'group' })` with `entityPresentationSnapshot(ref, { defaultKind: 'group' }).primaryTitle` in the OwnershipCard's `useGetEntities` hook. This was the last remaining usage of the deprecated function in the codebase. Signed-off-by: Marat Dyatko Made-with: Cursor Signed-off-by: Marat Dyatko Made-with: Cursor Signed-off-by: Marat Dyatko Made-with: Cursor --- .changeset/replace-humanize-entity-ref-org.md | 5 +++++ .../components/Cards/OwnershipCard/useGetEntities.test.ts | 2 +- .../src/components/Cards/OwnershipCard/useGetEntities.ts | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/replace-humanize-entity-ref-org.md diff --git a/.changeset/replace-humanize-entity-ref-org.md b/.changeset/replace-humanize-entity-ref-org.md new file mode 100644 index 0000000000..f2edfdf48a --- /dev/null +++ b/.changeset/replace-humanize-entity-ref-org.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Replaced deprecated `humanizeEntityRef` usage with the Catalog Presentation API. diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts index d7ee61b714..caa657b16d 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -50,7 +50,6 @@ jest.mock('@backstage/plugin-catalog-react', () => { return { catalogApiRef: {}, entityPresentationSnapshot: actual.entityPresentationSnapshot, - humanizeEntityRef: actual.humanizeEntityRef, getEntityRelations: jest.fn(entity => { return getEntityRelationsMock(entity); }) as any, @@ -291,6 +290,7 @@ describe('useGetEntities', () => { afterEach(() => { getEntityRelationsMock.mockRestore(); catalogApi.getEntities.mockRestore(); + catalogApi.getEntitiesByRefs.mockRestore(); }); it('should produce query params with humanized entity refs as owners', async () => { diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 56c1e8cebd..9a98ad788e 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -16,7 +16,6 @@ import { Entity, - parseEntityRef, RELATION_MEMBER_OF, RELATION_PARENT_OF, stringifyEntityRef, @@ -24,8 +23,8 @@ import { import { CatalogApi, catalogApiRef, + entityPresentationSnapshot, getEntityRelations, - humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import limiterFactory from 'p-limit'; import { useApi } from '@backstage/core-plugin-api'; @@ -47,8 +46,9 @@ const getQueryParams = ( selectedEntity: EntityTypeProps, ): string => { const { kind, type } = selectedEntity; - const owners = ownersEntityRef.map(owner => - humanizeEntityRef(parseEntityRef(owner), { defaultKind: 'group' }), + const owners = ownersEntityRef.map( + ref => + entityPresentationSnapshot(ref, { defaultKind: 'group' }).primaryTitle, ); const filters = { kind: kind.toLocaleLowerCase('en-US'),