Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HAI-1947 Show buttons in application view according to user permissions #381

Merged
merged 3 commits into from
Oct 6, 2023
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/domain/application/applicationView/ApplicationView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import { rest } from 'msw';
import { render, screen } from '../../../testUtils/render';
import { render, screen, waitFor } from '../../../testUtils/render';
import ApplicationViewContainer from './ApplicationViewContainer';
import { waitForLoadingToFinish } from '../../../testUtils/helperFunctions';
import { server } from '../../mocks/test-server';
import { SignedInUser } from '../../hanke/hankeUsers/hankeUser';

test('Correct information about application should be displayed', async () => {
render(<ApplicationViewContainer id={4} />);
@@ -56,7 +57,7 @@ test('Should show error notification if loading application fails', async () =>
test('Should be able to go editing application when editing is possible', async () => {
const { user } = render(<ApplicationViewContainer id={4} />);

await waitForLoadingToFinish();
await waitFor(() => screen.findByRole('button', { name: 'Muokkaa hakemusta' }));
await user.click(screen.getByRole('button', { name: 'Muokkaa hakemusta' }));

expect(window.location.pathname).toBe('/fi/johtoselvityshakemus/4/muokkaa');
@@ -73,8 +74,7 @@ test('Application edit button should not be displayed when editing is not possib
test('Should be able to cancel application if it is possible', async () => {
const { user } = render(<ApplicationViewContainer id={4} />);

await waitForLoadingToFinish();

await waitFor(() => screen.findByRole('button', { name: 'Peru hakemus' }));
await user.click(screen.getByRole('button', { name: 'Peru hakemus' }));
await user.click(screen.getByRole('button', { name: 'Vahvista' }));

@@ -90,3 +90,25 @@ test('Should not be able to cancel application if it has moved to handling in Al

expect(screen.queryByRole('button', { name: 'Peru hakemus' })).not.toBeInTheDocument();
});

test('Should not show Edit application and Cancel application buttons if user does not have EDIT_APPLICATIONS permission', async () => {
server.use(
rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(<ApplicationViewContainer id={4} />);

await waitForLoadingToFinish();

expect(screen.queryByRole('button', { name: 'Muokkaa hakemusta' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Peru hakemus' })).not.toBeInTheDocument();
});
31 changes: 18 additions & 13 deletions src/domain/application/applicationView/ApplicationView.tsx
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ import { ApplicationCancel } from '../components/ApplicationCancel';
import AttachmentSummary from '../components/AttachmentSummary';
import useAttachments from '../hooks/useAttachments';
import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags';
import UserRightsCheck from '../../hanke/hankeUsers/UserRightsCheck';

type Props = {
application: Application;
@@ -119,21 +120,25 @@ function ApplicationView({ application, hanke, onEditApplication }: Props) {

<InformationViewHeaderButtons>
{isPending ? (
<Button
theme="coat"
iconLeft={<IconPen aria-hidden="true" />}
onClick={onEditApplication}
>
{t('hakemus:buttons:editApplication')}
</Button>
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<Button
theme="coat"
iconLeft={<IconPen aria-hidden="true" />}
onClick={onEditApplication}
>
{t('hakemus:buttons:editApplication')}
</Button>
</UserRightsCheck>
) : null}
{hanke ? (
<ApplicationCancel
applicationId={id}
alluStatus={alluStatus}
hankeTunnus={hanke?.hankeTunnus}
buttonIcon={<IconTrash aria-hidden />}
/>
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<ApplicationCancel
applicationId={id}
alluStatus={alluStatus}
hankeTunnus={hanke?.hankeTunnus}
buttonIcon={<IconTrash aria-hidden />}
/>
</UserRightsCheck>
) : null}
</InformationViewHeaderButtons>
</InformationViewHeader>
4 changes: 2 additions & 2 deletions src/domain/hanke/accessRights/AccessRightsViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import ErrorLoadingText from '../../../common/components/errorLoadingText/ErrorL
import { useHankeUsers } from '../hankeUsers/hooks/useHankeUsers';
import useHanke from '../hooks/useHanke';
import AccessRightsView from './AccessRightsView';
import useSignedInUserRights from '../hankeUsers/hooks/useUserRights';
import useUserRightsForHanke from '../hankeUsers/hooks/useUserRightsForHanke';

type Props = {
hankeTunnus: string;
@@ -17,7 +17,7 @@ function AccessRightsViewContainer({ hankeTunnus }: Props) {
const { t } = useTranslation();
const { data: hankeUsers, isLoading, isError, error } = useHankeUsers(hankeTunnus);
const { data: hankeData } = useHanke(hankeTunnus);
const { data: signedInUser } = useSignedInUserRights(hankeTunnus);
const { data: signedInUser } = useUserRightsForHanke(hankeTunnus);

if (isLoading) {
return (
43 changes: 43 additions & 0 deletions src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { rest } from 'msw';
import { render, screen, waitFor } from '../../../testUtils/render';
import { server } from '../../mocks/test-server';
import { SignedInUser } from './hankeUser';
import UserRightsCheck from './UserRightsCheck';

test('Should render children if user has required right', async () => {
render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
});
});

test('Should not render children if user does not have required right', async () => {
server.use(
rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

await waitFor(() => {
expect(screen.queryByText('Children')).not.toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions src/domain/hanke/hankeUsers/UserRightsCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import useUserRightsForHanke from './hooks/useUserRightsForHanke';
import { Rights } from './hankeUser';

/**
* Check that user has required rights.
* If they have, render children.
*/
function UserRightsCheck({
requiredRight,
hankeTunnus,
children,
}: {
/** User right that is required to render children */
requiredRight: keyof typeof Rights;
/** hankeTunnus of the hanke that the right is required for */
hankeTunnus?: string;
children: React.ReactElement;
}) {
const { data: signedInUser } = useUserRightsForHanke(hankeTunnus);

if (signedInUser?.kayttooikeudet.includes(requiredRight)) {
return children;
}

return null;
}

export default UserRightsCheck;
22 changes: 12 additions & 10 deletions src/domain/hanke/hankeUsers/hankeUser.ts
Original file line number Diff line number Diff line change
@@ -14,16 +14,18 @@ export type HankeUser = {
tunnistautunut: boolean;
};

export type UserRights = Array<
| 'VIEW'
| 'MODIFY_VIEW_PERMISSIONS'
| 'EDIT'
| 'MODIFY_EDIT_PERMISSIONS'
| 'DELETE'
| 'MODIFY_DELETE_PERMISSIONS'
| 'EDIT_APPLICATIONS'
| 'MODIFY_APPLICATION_PERMISSIONS'
>;
export enum Rights {
VIEW = 'VIEW',
MODIFY_VIEW_PERMISSIONS = 'MODIFY_VIEW_PERMISSIONS',
EDIT = 'EDIT',
MODIFY_EDIT_PERMISSIONS = 'MODIFY_EDIT_PERMISSIONS',
DELETE = 'DELETE',
MODIFY_DELETE_PERMISSIONS = 'MODIFY_DELETE_PERMISSIONS',
EDIT_APPLICATIONS = 'EDIT_APPLICATIONS',
MODIFY_APPLICATION_PERMISSIONS = 'MODIFY_APPLICATION_PERMISSIONS',
}

export type UserRights = Array<keyof typeof Rights>;

export type SignedInUser = {
hankeKayttajaId: string;
4 changes: 2 additions & 2 deletions src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
@@ -17,8 +17,8 @@ export async function updateHankeUsers({
return data;
}

// Get user id and rights of the signed in user
export async function getSignedInUser(hankeTunnus?: string): Promise<SignedInUser> {
// Get user id and rights of the signed in user for a hanke
export async function getSignedInUserForHanke(hankeTunnus?: string): Promise<SignedInUser> {
const { data } = await api.get<SignedInUser>(`hankkeet/${hankeTunnus}/whoami`);
return data;
}
9 changes: 0 additions & 9 deletions src/domain/hanke/hankeUsers/hooks/useUserRights.ts

This file was deleted.

16 changes: 16 additions & 0 deletions src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery } from 'react-query';
import { SignedInUser } from '../hankeUser';
import { getSignedInUserForHanke } from '../hankeUsersApi';
import { useFeatureFlags } from '../../../../common/components/featureFlags/FeatureFlagsContext';

export default function useSignedInUserRightsForHanke(hankeTunnus?: string) {
const features = useFeatureFlags();

return useQuery<SignedInUser>(
['signedInUser', hankeTunnus],
() => getSignedInUserForHanke(hankeTunnus),
{
enabled: Boolean(hankeTunnus) && features.accessRights,
},
);
}
4 changes: 2 additions & 2 deletions src/domain/hanke/hankeView/HankeViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -5,15 +5,15 @@ import { ROUTES } from '../../../common/types/route';
import HankeDelete from '../edit/components/HankeDelete';
import useHanke from '../hooks/useHanke';
import HankeView from './HankeView';
import useSignedInUserRights from '../hankeUsers/hooks/useUserRights';
import useUserRightsForHanke from '../hankeUsers/hooks/useUserRightsForHanke';

type Props = {
hankeTunnus?: string;
};

const HankeViewContainer: React.FC<Props> = ({ hankeTunnus }) => {
const { data: hankeData } = useHanke(hankeTunnus);
const { data: signedInUser } = useSignedInUserRights(hankeTunnus);
const { data: signedInUser } = useUserRightsForHanke(hankeTunnus);
const getEditHankePath = useLinkPath(ROUTES.EDIT_HANKE);
const getEditRightsPath = useLinkPath(ROUTES.ACCESS_RIGHTS);
const navigate = useNavigate();
24 changes: 16 additions & 8 deletions src/domain/hanke/portfolio/HankePortfolio.test.tsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ const endDateLabel = 'Ajanjakson loppu';

afterEach(cleanup);

jest.setTimeout(20000);
jest.setTimeout(30000);

describe.only('HankePortfolio', () => {
test('Changing search text filters correct number of projects', async () => {
@@ -30,7 +30,7 @@ describe.only('HankePortfolio', () => {
});
expect(screen.getByTestId('numberOfFilteredRows')).toHaveTextContent('0');
expect(
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta')
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'),
).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /tyhjennä hakuehdot/i }));
@@ -50,7 +50,7 @@ describe.only('HankePortfolio', () => {
changeFilterDate(startDateLabel, renderedComponent, '11.10.2022');
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0');
expect(
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta')
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'),
).toBeInTheDocument();
changeFilterDate(startDateLabel, renderedComponent, null);
});
@@ -61,7 +61,7 @@ describe.only('HankePortfolio', () => {
changeFilterDate(endDateLabel, renderedComponent, '01.10.2022');
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0');
expect(
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta')
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'),
).toBeInTheDocument();
changeFilterDate(endDateLabel, renderedComponent, '05.10.2022');
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('2');
@@ -75,22 +75,22 @@ describe.only('HankePortfolio', () => {
const renderedComponent = render(<HankePortfolioComponent hankkeet={hankeList} />);
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('2');
await renderedComponent.user.click(
renderedComponent.getByRole('button', { name: 'Työn tyyppi' })
renderedComponent.getByRole('button', { name: 'Työn tyyppi' }),
);
await renderedComponent.user.click(renderedComponent.getByText('Sähkö'));
renderedComponent.getByText('Hankevaiheet').click();
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('0');
expect(
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta')
screen.queryByText('Valitsemillasi hakuehdoilla ei löytynyt yhtään hanketta'),
).toBeInTheDocument();
await renderedComponent.user.click(
renderedComponent.getByRole('button', { name: 'Työn tyyppi' })
renderedComponent.getByRole('button', { name: 'Työn tyyppi' }),
);
await renderedComponent.user.click(renderedComponent.getByText('Viemäri'));
renderedComponent.getByText('Hankevaiheet').click();
expect(renderedComponent.getByTestId('numberOfFilteredRows')).toHaveTextContent('1');
await renderedComponent.user.click(
renderedComponent.getByRole('button', { name: 'Työn tyyppi' })
renderedComponent.getByRole('button', { name: 'Työn tyyppi' }),
);
await renderedComponent.user.click(renderedComponent.getByText('Sadevesi'));
renderedComponent.getByText('Hankevaiheet').click();
@@ -102,4 +102,12 @@ describe.only('HankePortfolio', () => {

expect(screen.queryByText('Hankesalkussasi ei ole hankkeita')).toBeInTheDocument();
});

test('Should render edit hanke links for hankkeet that user has edit rights', async () => {
render(<HankePortfolioComponent hankkeet={hankeList} />);

await waitFor(() => {
expect(screen.queryAllByTestId('hankeEditLink')).toHaveLength(1);
});
});
});
25 changes: 14 additions & 11 deletions src/domain/hanke/portfolio/HankePortfolioComponent.tsx
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ import { SKIP_TO_ELEMENT_ID } from '../../../common/constants/constants';
import useHankeViewPath from '../hooks/useHankeViewPath';
import { useNavigateToApplicationList } from '../hooks/useNavigateToApplicationList';
import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags';
import UserRightsCheck from '../hankeUsers/UserRightsCheck';

type CustomAccordionProps = {
hanke: HankeData;
@@ -114,17 +115,19 @@ const CustomAccordion: React.FC<React.PropsWithChildren<CustomAccordionProps>> =
<IconEye aria-hidden />
</Link>
<FeatureFlags flags={['hanke']}>
<Link
to={getEditHankePath({ hankeTunnus: hanke.hankeTunnus })}
aria-label={
// eslint-disable-next-line
t(`routes:${ROUTES.EDIT_HANKE}.meta.title`) +
` ${hanke.nimi} - ${hanke.hankeTunnus} `
}
data-testid="hankeEditLink"
>
<IconPen aria-hidden />
</Link>
<UserRightsCheck requiredRight="EDIT" hankeTunnus={hanke.hankeTunnus}>
<Link
to={getEditHankePath({ hankeTunnus: hanke.hankeTunnus })}
aria-label={
// eslint-disable-next-line
t(`routes:${ROUTES.EDIT_HANKE}.meta.title`) +
` ${hanke.nimi} - ${hanke.hankeTunnus} `
}
data-testid="hankeEditLink"
>
<IconPen aria-hidden />
</Link>
</UserRightsCheck>
</FeatureFlags>
</div>
<button type="button" className={styles.iconWrapper}>
Loading