diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index a290adbaee..81bbb21438 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -15,117 +15,191 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { CatalogApi } from '@backstage/catalog-client'; import { FieldProps } from '@rjsf/core'; -import { EntityPickerProps } from '../EntityPicker/schema'; -import { Entity } from '@backstage/catalog-model'; import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import userEvent from '@testing-library/user-event'; -const makeUserEntity = ( - kind: string, - memberOf: string[], - name: string, -): Entity => ({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind, - metadata: { name }, - spec: { - memberOf: memberOf, - }, -}); - -const makeGroupEntity = ( - kind: string, - members: string[], - name: string, -): Entity => ({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind, - metadata: { name }, - spec: { - members: members, - }, -}); +// 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(), +}; describe('', () => { let entities: Entity[]; const onChange = jest.fn(); const schema = {}; const required = false; - let uiSchema: EntityPickerProps['uiSchema']; - const rawErrors: string[] = []; - const formData = undefined; - - let props: FieldProps; const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - addLocation: jest.fn(), - getLocationByRef: jest.fn(), - removeEntityByUid: jest.fn(), + getEntities: jest.fn(async () => ({ items: entities })), } as any; beforeEach(() => { entities = [ - makeUserEntity('User', ['group1', 'group2'], 'Bob'), - makeGroupEntity('Group', ['Alice', 'Dave'], 'group3'), + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group3' }, + spec: { members: ['Alice'] }, + }, ]; + + onChange.mockClear(); + catalogApi.getEntities.mockClear(); }); - afterEach(() => jest.resetAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + }); - it('should only return the groups a user is part of', async () => { - catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); + it('should only return the groups a user is part of and not the groups a user is not part of', async () => { + const userGroups = entities.filter( + entity => + entity.spec && + Array.isArray(entity.spec.members) && + entity.spec.members.includes('Bob'), + ); - uiSchema = { 'ui:options': { catalogApi } }; - props = { + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + + const props = { onChange, schema, required, - uiSchema, - rawErrors, - formData, } as unknown as FieldProps; - render(); + render( + + + , + ); - expect(onChange).toHaveBeenCalledWith(['group1', 'group2']); + await waitFor(() => + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: { + kind: ['Group'], + 'relations.hasMember': ['group:default/group1', 'group:default/group2'], + }, + }); + + // Check that getEntities was set up to return the correct data + await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual( + { + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + ], + }, + ); + + await expect( + catalogApi.getEntities.mock.results[0].value, + ).resolves.not.toEqual( + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + metadata: { name: 'group3' }, + }), + ]), + }), + ); }); - it('should not return groups a user is not part of', async () => { - // Mock 'getEntityByRef' to return the entity that represents Bob - catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); + it('should display the groups a user is part of and not display the groups a user is not part of', async () => { + const userGroups = entities.filter( + entity => + entity.spec && + Array.isArray(entity.spec.members) && + entity.spec.members.includes('Bob'), + ); + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); - uiSchema = { 'ui:options': { catalogApi } }; - props = { + const props = { onChange, schema, required, - uiSchema, - rawErrors, - formData, } as unknown as FieldProps; - render(); + const { queryByText, getByRole } = render( + + + , + ); - // The onChange should not have been called with 'group3', since Bob is not part of 'group3' - expect(onChange).not.toHaveBeenCalledWith(['group3']); - }); + await waitFor(() => + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + ); - it('should render without imploding', () => { - uiSchema = { 'ui:options': { catalogApi } }; - props = { - onChange, - schema, - required, - uiSchema, - rawErrors, - formData, - } as unknown as FieldProps; + // Simulate user input + const inputField = getByRole('combobox'); + userEvent.click(inputField); + userEvent.type(inputField, 'group'); - const { container } = render(); - expect(container).not.toBeNull(); + // Wait for the dropdown elements to appear + await waitFor(() => { + const group1Element = queryByText('group1'); + const group2Element = queryByText('group2'); + expect(group1Element).toBeInTheDocument(); + expect(group2Element).toBeInTheDocument(); + }); + + // Assert that 'group3' is not rendered in the component + expect(queryByText('group3')).not.toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index 5fee57eb86..f89c6ff789 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -30,12 +30,16 @@ export { OwnershipEntityRefPickerSchema }; export const OwnershipEntityRefPicker = ( props: OwnershipEntityRefPickerProps, ) => { - const { uiSchema, required, rawErrors, formData } = props; + const { + schema: { title, description }, + required, + rawErrors, + } = props; const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); const [groups, setGroups] = useState([]); - const [selectedGroup, setSelectedGroup] = useState(''); + const [selectedGroup, setSelectedGroup] = useState(null); useEffect(() => { const fetchUserGroups = async () => { @@ -63,20 +67,20 @@ export const OwnershipEntityRefPicker = ( 0 && !formData} + error={rawErrors?.length > 0} > setSelectedGroup(value || '')} getOptionLabel={group => group} renderInput={params => (