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

Handle missing profile when the logged in client in not a user (e.g. service owner) #2889

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions src/__mocks__/getPartyMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ export const getPartyMock = (): IParty => ({
childParties: undefined,
});

export const ServiceOwnerPartyId = 414234123;
export const getServiceOwnerPartyMock = (): IParty => ({
partyId: ServiceOwnerPartyId,
name: 'Brønnøysundregistrene',
ssn: null,
partyTypeName: PartyType.Organisation,
orgNumber: '974760673',
unitType: 'BEDR',
isDeleted: false,
onlyHierarchyElementWithNoAccess: false,
childParties: undefined,
});

export type PartyWithSubunit = { org: IParty & { childParties: IParty[] }; person: IParty };
export const getPartyWithSubunitMock = (): PartyWithSubunit => ({
org: {
Expand Down
3 changes: 1 addition & 2 deletions src/features/instantiate/containers/InstantiateContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';

import { isAxiosError } from 'axios';

import { Loader } from 'src/core/loading/Loader';
import { InstantiateValidationError } from 'src/features/instantiate/containers/InstantiateValidationError';
import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError';
Expand All @@ -10,6 +8,7 @@ import { useInstantiation } from 'src/features/instantiate/InstantiationContext'
import { useCurrentParty } from 'src/features/party/PartiesProvider';
import { AltinnAppTheme } from 'src/theme/altinnAppTheme';
import { changeBodyBackground } from 'src/utils/bodyStyling';
import { isAxiosError } from 'src/utils/isAxiosError';
import { HttpStatusCodes } from 'src/utils/network/networking';

export const InstantiateContainer = () => {
Expand Down
19 changes: 16 additions & 3 deletions src/features/language/LanguageProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface LanguageCtx {
current: string;
profileLoaded: boolean;
updateProfile: (profile: IProfile) => void;
noProfileFound: () => void;
setWithLanguageSelector: (language: string) => void;
}

Expand All @@ -20,6 +21,9 @@ const { Provider, useCtx } = createContext<LanguageCtx>({
updateProfile: () => {
throw new Error('LanguageProvider not initialized');
},
noProfileFound: () => {
throw new Error('LanguageProvider not initialized');
},
setWithLanguageSelector: () => {
throw new Error('LanguageProvider not initialized');
},
Expand All @@ -46,19 +50,28 @@ export const LanguageProvider = ({ children }: PropsWithChildren) => {
localStorage.setItem(localStorageKey, newLanguage);
};

const noProfileFound = () => {
// Just mark it as loaded, so we can continue loading language resources
setProfileLoaded(true);
};

const setWithLanguageSelector = (language: string) => {
setCurrent(language);
localStorage.setItem(`selectedAppLanguage${window.app}${userId}`, language);
};

return <Provider value={{ current, profileLoaded, updateProfile, setWithLanguageSelector }}>{children}</Provider>;
return (
<Provider value={{ current, profileLoaded, updateProfile, setWithLanguageSelector, noProfileFound }}>
{children}
</Provider>
);
};

export const useCurrentLanguage = () => useCtx().current;
export const useIsProfileLanguageLoaded = () => useCtx().profileLoaded;
export const useSetCurrentLanguage = () => {
const { setWithLanguageSelector, updateProfile } = useCtx();
return { setWithLanguageSelector, updateProfile };
const { setWithLanguageSelector, updateProfile, noProfileFound } = useCtx();
return { setWithLanguageSelector, updateProfile, noProfileFound };
};

function getLanguageQueryParam() {
Expand Down
127 changes: 127 additions & 0 deletions src/features/pdf/PDFWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React from 'react';

import { jest } from '@jest/globals';
import { screen, waitFor } from '@testing-library/react';
import type { AxiosError } from 'axios';

import { getIncomingApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock';
import { getInstanceDataMock } from 'src/__mocks__/getInstanceDataMock';
import { getPartyMock, getServiceOwnerPartyMock } from 'src/__mocks__/getPartyMock';
import { getProcessDataMock } from 'src/__mocks__/getProcessDataMock';
import { getProfileMock } from 'src/__mocks__/getProfileMock';
import { ProcessWrapper } from 'src/components/wrappers/ProcessWrapper';
import { InstanceProvider } from 'src/features/instance/InstanceContext';
import { fetchApplicationMetadata, fetchProcessState } from 'src/queries/queries';
import { InstanceRouter, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders';
import type { AppQueries } from 'src/queries/types';

const exampleGuid = '75154373-aed4-41f7-95b4-e5b5115c2edc';
const exampleInstanceId = `512345/${exampleGuid}`;

enum RenderAs {
User,
ServiceOwner,
}

const axios400Error: AxiosError = {
isAxiosError: true,
code: 'ERR_BAD_REQUEST',
status: 400,
response: {
status: 400,
statusText: 'Bad Request',
headers: {},
config: null!,
data: undefined,
},
name: 'AxiosError',
message: 'Request failed with status code 400',
toJSON: () => ({}),
};

const buildInstance = () =>
getInstanceDataMock((i) => {
i.org = 'brg';
i.id = exampleInstanceId;
i.lastChanged = '2022-02-05T09:19:32.8858042Z';
});

const render = async (renderAs: RenderAs, queriesOverride?: Partial<AppQueries>) => {
jest.mocked(fetchApplicationMetadata).mockImplementationOnce(async () =>
getIncomingApplicationMetadataMock((m) => {
m.org = 'brg';
m.partyTypesAllowed.person = true;
m.partyTypesAllowed.organisation = true;
}),
);
jest.mocked(fetchProcessState).mockImplementation(async () =>
getProcessDataMock((p) => {
p.processTasks = [p.currentTask!];
}),
);

const party = renderAs === RenderAs.User ? getPartyMock() : getServiceOwnerPartyMock();

return await renderWithoutInstanceAndLayout({
renderer: () => (
<InstanceProvider>
<ProcessWrapper />
</InstanceProvider>
),
router: ({ children }) => (
<InstanceRouter
instanceId={exampleInstanceId}
taskId='Task_1'
initialPage=''
query='pdf=1'
>
{children}
</InstanceRouter>
),
queries: {
fetchOrgs: async () => ({
orgs: {
brg: {
name: {
en: 'Brønnøysund Register Centre',
nb: 'Brønnøysundregistrene',
nn: 'Brønnøysundregistera',
},
logo: 'https://altinncdn.no/orgs/brg/brreg.png',
orgnr: '974760673',
homepage: 'https://www.brreg.no',
environments: ['tt02', 'production'],
},
},
}),
fetchInstanceData: async () => buildInstance(),
fetchFormData: async () => ({}),
fetchLayouts: async () => ({}),
fetchCurrentParty: async () => party,
fetchParties: async () => [party],
fetchUserProfile: async () => {
if (renderAs === RenderAs.User) {
return getProfileMock();
}
throw axios400Error;
},
...queriesOverride,
},
});
};

describe('PDFWrapper', () => {
it.each([RenderAs.User, RenderAs.ServiceOwner])(`should render PDF - %s`, async (renderAs) => {
const result = await render(renderAs);

await waitFor(() => expect(result.container.querySelector('#readyForPrint')).not.toBeNull(), { timeout: 5000 });

if (renderAs === RenderAs.ServiceOwner) {
expect(await screen.queryByText('Avsender:')).toBeNull();
expect(await screen.queryByText('01017512345-Ola Privatperson')).toBeNull();
} else if (renderAs === RenderAs.User) {
expect(await screen.queryByText('Avsender:')).not.toBeNull();
expect(await screen.queryByText('01017512345-Ola Privatperson')).not.toBeNull();
}
});
});
17 changes: 15 additions & 2 deletions src/features/profile/ProfileProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useEffect } from 'react';

import { useQuery } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';

import { useAppQueries } from 'src/core/contexts/AppQueriesProvider';
import { delayedContext } from 'src/core/contexts/delayedContext';
import { createQueryContext } from 'src/core/contexts/queryContext';
import { useSetCurrentLanguage } from 'src/features/language/LanguageProvider';
import { useAllowAnonymousIs } from 'src/features/stateless/getAllowAnonymous';
import { isAxiosError } from 'src/utils/isAxiosError';
import type { IProfile } from 'src/types/shared';

// Also used for prefetching @see appPrefetcher.ts
Expand All @@ -19,15 +21,25 @@ export function useProfileQueryDef(enabled: boolean) {
};
}

const canHandleProfileQueryError = (error: UseQueryResult<IProfile | undefined>['error']) =>
// The backend will return 400 if the logged in user/client is not a user.
// Altinn users have profiles, but organisations, service owners and system users do not, so this is expected
isAxiosError(error) && error.response?.status === 400;

const useProfileQuery = () => {
const enabled = useShouldFetchProfile();
const { updateProfile } = useSetCurrentLanguage();
const { updateProfile, noProfileFound } = useSetCurrentLanguage();

const utils = useQuery(useProfileQueryDef(enabled));

useEffect(() => {
if (canHandleProfileQueryError(utils.error)) {
noProfileFound();
return;
}

utils.error && window.logError('Fetching user profile failed:\n', utils.error);
}, [utils.error]);
}, [noProfileFound, utils.error]);

useEffect(() => {
if (utils.data) {
Expand All @@ -46,6 +58,7 @@ const { Provider, useCtx } = delayedContext(() =>
name: 'Profile',
required: false,
default: undefined,
shouldDisplayError: (error) => !canHandleProfileQueryError(error),
query: useProfileQuery,
}),
);
Expand Down
5 changes: 4 additions & 1 deletion src/test/renderWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ interface InstanceRouterProps {
taskId?: string;
instanceId?: string;
alwaysRouteToChildren?: boolean;
query?: string;
}

interface ExtendedRenderOptionsWithInstance extends ExtendedRenderOptions, InstanceRouterProps {}
Expand Down Expand Up @@ -246,11 +247,13 @@ export function InstanceRouter({
taskId = 'Task_1',
initialPage = 'FormLayout',
alwaysRouteToChildren = false,
query,
}: PropsWithChildren<InstanceRouterProps>) {
const path = `/ttd/test/instance/${instanceId}/${taskId}/${initialPage}`;
return (
<MemoryRouter
basename='/ttd/test'
initialEntries={[`/ttd/test/instance/${instanceId}/${taskId}/${initialPage}`]}
initialEntries={[query ? `${path}?${query}` : path]}
>
<Routes>
<Route
Expand Down
Loading