diff --git a/src/apps/work/src/lib/utils/permissions.utils.spec.ts b/src/apps/work/src/lib/utils/permissions.utils.spec.ts index bf78e999f..309998ef2 100644 --- a/src/apps/work/src/lib/utils/permissions.utils.spec.ts +++ b/src/apps/work/src/lib/utils/permissions.utils.spec.ts @@ -54,6 +54,11 @@ describe('permissions.utils project management helpers', () => { .toBe(true) }) + it('allows project managers to create projects without a project context', () => { + expect(checkCanManageProject(['Project Manager'], '123')) + .toBe(true) + }) + it('requires project manager or copilot membership for talent manager edit access', () => { expect(checkCanManageProject(['Talent Manager'], '123', managedProject)) .toBe(true) @@ -71,11 +76,6 @@ describe('permissions.utils project management helpers', () => { .toBe(false) }) - it('does not expand project-manager creation access beyond the work-manager change', () => { - expect(checkCanManageProject(['Project Manager'], '123')) - .toBe(false) - }) - it('limits engagement creation to admins and talent managers', () => { expect(canCreateEngagement(['copilot'])) .toBe(false) diff --git a/src/apps/work/src/lib/utils/permissions.utils.ts b/src/apps/work/src/lib/utils/permissions.utils.ts index efd20b778..b2107c008 100644 --- a/src/apps/work/src/lib/utils/permissions.utils.ts +++ b/src/apps/work/src/lib/utils/permissions.utils.ts @@ -264,9 +264,9 @@ export function checkProjectMembership( /** * Returns whether the caller can manage project ownership and billing flows. * - * Admins always qualify. Copilots and Talent Managers can create projects. - * When a project context is provided, Project Managers may also manage that - * existing project when they hold a manager or copilot membership on it. + * Admins always qualify. Copilots, Project Managers, and Talent Managers can + * create projects. Managing an existing project still requires a manager or + * copilot membership on that project. * * @param userRoles caller roles from the decoded auth token or app context. * @param userId logged-in user identifier used for project membership checks. @@ -283,7 +283,7 @@ export function checkCanManageProject( } if (!project) { - return hasCopilotRole(userRoles) || checkTalentManager(userRoles) + return hasCopilotRole(userRoles) || hasManagerRole(userRoles) } if (!hasCopilotRole(userRoles) && !hasManagerRole(userRoles)) { diff --git a/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx new file mode 100644 index 000000000..79f069146 --- /dev/null +++ b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx @@ -0,0 +1,158 @@ +/* eslint-disable no-var, global-require, @typescript-eslint/no-var-requires */ +/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ +import type { Context, PropsWithChildren, ReactNode } from 'react' +import { + render, + screen, +} from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' + +import { WorkAppContextModel } from '../../../lib/models/WorkAppContextModel.model' +import { useFetchProjectsList } from '../../../lib/hooks' + +import { ProjectsListPage } from './ProjectsListPage' + +var mockWorkAppContext: Context + +jest.mock('react-toastify', () => ({ + toast: { + error: jest.fn(), + }, +})) +jest.mock('tc-auth-lib', () => ({ + decodeToken: jest.fn(), +})) +jest.mock('../../../lib/services/resources.service', () => ({ + fetchResourceRoles: jest.fn(), + fetchResources: jest.fn(), +})) +jest.mock('~/apps/admin/src/lib', () => ({ + TableLoading: () =>
Loading
, +}), { + virtual: true, +}) +jest.mock('~/apps/review/src/lib', () => ({ + PageWrapper: ( + props: PropsWithChildren<{ pageTitle?: string; rightHeader?: ReactNode }>, + ) => ( +
+
{props.rightHeader}
+

{props.pageTitle}

+
{props.children}
+
+ ), +}), { + virtual: true, +}) +jest.mock('~/libs/ui', () => ({ + Button: (props: { label: string }) => ( + + ), +}), { + virtual: true, +}) +jest.mock('../../../lib/components', () => ({ + Pagination: () =>
Pagination
, + ProjectsFilter: () =>
Projects Filter
, + ProjectsTable: () =>
Projects Table
, +})) +jest.mock('../../../lib/contexts', () => { + const React = require('react') as typeof import('react') + + mockWorkAppContext = React.createContext({ + isAdmin: false, + isAnonymous: false, + isCopilot: false, + isManager: false, + isReadOnly: false, + loginUserInfo: undefined, + userRoles: [], + }) + + return { + WorkAppContext: mockWorkAppContext, + } +}) +jest.mock('../../../lib/hooks', () => ({ + useFetchProjectsList: jest.fn(), +})) +jest.mock('../../../lib/constants', () => ({ + PROJECTS_PAGE_SIZE: 10, +})) +jest.mock('../../../lib/utils', () => ({ + checkCanManageProject: jest.requireActual('../../../lib/utils/permissions.utils').checkCanManageProject, +})) + +const mockedUseFetchProjectsList = useFetchProjectsList as jest.Mock + +const defaultContextValue: WorkAppContextModel = { + isAdmin: false, + isAnonymous: false, + isCopilot: false, + isManager: false, + isReadOnly: false, + loginUserInfo: { + email: 'user@example.com', + exp: 0, + handle: 'work-user', + iat: 0, + roles: ['topcoder user'], + userId: 12345, + } as WorkAppContextModel['loginUserInfo'], + userRoles: ['topcoder user'], +} + +const projectManagerContextValue: WorkAppContextModel = { + ...defaultContextValue, + isManager: true, + loginUserInfo: { + ...defaultContextValue.loginUserInfo, + roles: ['project manager'], + } as WorkAppContextModel['loginUserInfo'], + userRoles: ['project manager'], +} + +function renderPage(contextValue: WorkAppContextModel = defaultContextValue): void { + const MockWorkAppContext = mockWorkAppContext + + render( + + + + + , + ) +} + +describe('ProjectsListPage', () => { + beforeEach(() => { + jest.clearAllMocks() + + mockedUseFetchProjectsList.mockReturnValue({ + error: undefined, + isLoading: false, + isValidating: false, + metadata: { + page: 1, + perPage: 10, + total: 0, + }, + mutate: jest.fn(), + projects: [], + }) + }) + + it('renders the new project action for project managers', () => { + renderPage(projectManagerContextValue) + + const newProjectButton = screen.getByRole('button', { name: 'New Project' }) + const newProjectLink = screen.getByRole('link', { name: 'New Project' }) + + expect(newProjectButton) + .toBeTruthy() + expect(newProjectLink.getAttribute('href')) + .toBe('/projects/new') + }) +})