Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { HTTP_METHODS } from '../../data-access/types/haapi-form.types';
import { MEDIA_TYPES } from '../../../shared/util/types/media.types';
import {
authenticationStep,
completedWithErrorStep,
completedWithErrorStepWithoutLinks,
completedWithSuccessStep,
completedWithSuccessStepWithoutLinks,
continueSameStep,
Expand All @@ -30,6 +32,7 @@ import { useHaapiStepper } from './HaapiStepperHook';
import type { HaapiStepperHistoryEntry, HaapiStepperNextStepAction } from './haapi-stepper.types';
import { HaapiStepperActionStep, HaapiStepperFormAction } from './haapi-stepper.types';
import type { BootstrapConfiguration } from '../../data-access/bootstrap-configuration';
import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from './typings';

describe('HaapiStepper', () => {
const initialStepType = HAAPI_STEPS.AUTHENTICATION;
Expand Down Expand Up @@ -372,79 +375,126 @@ describe('HaapiStepper', () => {
});
});

describe('Completed With Success Step', () => {
const authorizationResponseUrl = completedWithSuccessStep.links?.find(
link => link.rel === 'authorization-response'
)?.href;

describe('redirectOnAuthenticationCompletedWithSuccess enabled (default)', () => {
it('should redirect to the authorization-response URL', async () => {
describe.each([
{
label: 'success',
stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS,
stepFixture: completedWithSuccessStep,
},
{
label: 'error',
stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR,
stepFixture: completedWithErrorStep,
},
] as const)('Completed With $label Step', ({ label, stepType, stepFixture }) => {
const authorizationResponseUrl = stepFixture.links?.find(link => link.rel === 'authorization-response')?.href;
const redirectSetting =
label === 'success'
? AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS
: AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR;
const skipRedirectSetting =
label === 'success'
? AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR
: AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS;

describe('autoRedirectOnAuthenticationComplete', () => {
it('redirects to the authorization-response URL when set to true (default)', async () => {
render(
<HaapiStepper>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS);
await goToNextStep(stepType);

await waitFor(() => {
expect(window.location.href).toBe(authorizationResponseUrl);
});
});

it('should throw error to the error boundary when no authorization-response link exists', async () => {
it(`redirects when set to '${redirectSetting}'`, async () => {
render(
<HaapiStepper>
<HaapiStepper config={{ autoRedirectOnAuthenticationComplete: redirectSetting }}>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS, { noLinks: true });
await goToNextStep(stepType);

await waitFor(() => {
expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith(
'redirectOnAuthenticationCompletedWithSuccess is enabled, but the completed-with-success step did not include an authorization-response link.'
);
expect(window.location.href).toBe(authorizationResponseUrl);
});
});

it('should not update the current step when redirecting', async () => {
it('renders the completed step instead of redirecting when set to false', async () => {
render(
<HaapiStepper>
<HaapiStepper config={{ autoRedirectOnAuthenticationComplete: false }}>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS);
await goToNextStep(stepType);

await waitFor(() => {
expect(window.location.href).toBe(authorizationResponseUrl);
expect(screen.getByTestId('step-type')).toHaveTextContent(stepType);
});

expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType);
expect(window.location.href).not.toBe(authorizationResponseUrl);
});
});

describe('redirectOnAuthenticationCompletedWithSuccess disabled', () => {
it('should render the completed step instead of redirecting', async () => {
it(`renders the completed step instead of redirecting when set to '${skipRedirectSetting}'`, async () => {
render(
<HaapiStepper config={{ redirectOnAuthenticationCompletedWithSuccess: false }}>
<HaapiStepper config={{ autoRedirectOnAuthenticationComplete: skipRedirectSetting }}>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(HAAPI_STEPS.COMPLETED_WITH_SUCCESS);
await goToNextStep(stepType);

await waitFor(() => {
expect(screen.getByTestId('step-type')).toHaveTextContent(HAAPI_STEPS.COMPLETED_WITH_SUCCESS);
expect(screen.getByTestId('step-type')).toHaveTextContent(stepType);
});

expect(window.location.href).not.toBe(authorizationResponseUrl);
});

it('throws to the error boundary when redirect is enabled but no authorization-response link exists', async () => {
render(
<HaapiStepper>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(stepType, { noLinks: true });

await waitFor(() => {
expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith(
`autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${label} step did not include an authorization-response link.`
);
});
});

it('does not update the current step when redirecting', async () => {
render(
<HaapiStepper>
<TestComponent />
</HaapiStepper>
);

await screen.findByTestId('step-type');
await goToNextStep(stepType);

await waitFor(() => {
expect(window.location.href).toBe(authorizationResponseUrl);
});

expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType);
});
});
});
});
Expand Down Expand Up @@ -810,6 +860,9 @@ function getStepMock(stepType: HAAPI_STEPS | HAAPI_PROBLEM_STEPS, config?: Recor
case HAAPI_STEPS.COMPLETED_WITH_SUCCESS:
stepMock = config?.noLinks ? completedWithSuccessStepWithoutLinks : completedWithSuccessStep;
break;
case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR:
stepMock = config?.noLinks ? completedWithErrorStepWithoutLinks : completedWithErrorStep;
break;
case HAAPI_STEPS.REGISTRATION:
stepMock = createRegistrationStep();
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import { formatContinueSameStepData } from './data-formatters/continue-same-step
import { handlePollingStep } from './data-formatters/polling-step';
import { formatErrorStepData } from './data-formatters/problem-step';
import { formatNextStepData } from './data-formatters/format-next-step-data';
import { handleCompletedWithSuccessStep } from './step-handlers/completed-with-success-step';
import { handleCompletedStep } from './step-handlers/completed-step';
import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from './typings';
import { sendHaapiFetchRequest } from '../../data-access/happi-fetch-request';
import { configuration } from '../../data-access/bootstrap-configuration';
import type {
Expand All @@ -46,13 +47,13 @@ import { useRefCallback } from '../../util/useRefCallBack';
const DEFAULT_CONFIG: Required<HaapiStepperConfig> = {
pollingInterval: 3000,
bankIdAutostart: true,
redirectOnAuthenticationCompletedWithSuccess: true,
autoRedirectOnAuthenticationComplete: true,
};

export interface HaapiStepperConfig {
pollingInterval: number;
bankIdAutostart: boolean;
redirectOnAuthenticationCompletedWithSuccess: boolean;
autoRedirectOnAuthenticationComplete: boolean | AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE;
}

interface HaapiStepperProps {
Expand Down Expand Up @@ -404,14 +405,10 @@ async function processHaapiNextStep(
case HAAPI_STEPS.POLLING:
return handlePollingStep(nextStepResponse, pendingOperation, nextStep, config, history);

case HAAPI_STEPS.COMPLETED_WITH_SUCCESS:
return handleCompletedWithSuccessStep(nextStepResponse, config);

case HAAPI_STEPS.AUTHENTICATION:
case HAAPI_STEPS.REGISTRATION:
case HAAPI_STEPS.USER_CONSENT:
case HAAPI_STEPS.CONSENTOR:
case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR:
return { nextStepData: formatNextStepData(nextStepResponse) };

case HAAPI_STEPS.CONTINUE_SAME:
Expand All @@ -420,6 +417,10 @@ async function processHaapiNextStep(
}
return { nextStepData: formatContinueSameStepData(action, nextStepResponse, currentStep as HaapiStepperStep) };

case HAAPI_STEPS.COMPLETED_WITH_SUCCESS:
case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR:
return handleCompletedStep(nextStepResponse, config);

case HAAPI_PROBLEM_STEPS.INVALID_INPUT:
case HAAPI_PROBLEM_STEPS.INCORRECT_CREDENTIALS:
case HAAPI_PROBLEM_STEPS.AUTHENTICATION_FAILED:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2025 Curity AB. All rights reserved.
*
* The contents of this file are the property of Curity AB.
* You may not copy or use this file, in either source code
* or executable form, except in compliance with terms
* set by Curity AB.
*
* For further information, please contact Curity AB.
*/

import { HAAPI_PROBLEM_STEPS, HAAPI_STEPS, type HaapiCompletedStep } from '../../../data-access/types/haapi-step.types';
import type { HaapiStepperConfig } from '../HaapiStepper';
import { AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE } from '../typings';
import { formatNextStepData } from '../data-formatters/format-next-step-data';

export function handleCompletedStep(step: HaapiCompletedStep, config: HaapiStepperConfig) {
const isSuccess = step.type === HAAPI_STEPS.COMPLETED_WITH_SUCCESS;
const isError = step.type === HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR;
const setting = config.autoRedirectOnAuthenticationComplete;
const shouldRedirect =
setting === true ||
(setting === AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_SUCCESS && isSuccess) ||
(setting === AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE.ONLY_ON_ERROR && isError);

if (!shouldRedirect) {
return { nextStepData: formatNextStepData(step) };
}

const redirectHref = step.links?.find(link => link.rel === 'authorization-response')?.href;

if (!redirectHref) {
throw new Error(
`autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${isSuccess ? 'success' : 'error'} step did not include an authorization-response link.`
);
}

window.location.href = redirectHref;
return { nextStepData: undefined };
}

This file was deleted.

15 changes: 15 additions & 0 deletions src/login-web-app/src/haapi-stepper/feature/stepper/typings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2025 Curity AB. All rights reserved.
*
* The contents of this file are the property of Curity AB.
* You may not copy or use this file, in either source code
* or executable form, except in compliance with terms
* set by Curity AB.
*
* For further information, please contact Curity AB.
*/

export enum AUTO_REDIRECT_ON_AUTHENTICATION_COMPLETE {
ONLY_ON_SUCCESS = 'ONLY_ON_SUCCESS',
ONLY_ON_ERROR = 'ONLY_ON_ERROR',
}
19 changes: 19 additions & 0 deletions src/login-web-app/src/shared/util/api-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,25 @@ export const completedWithSuccessStepWithoutLinks = {
} as HaapiCompletedWithSuccessStep;

export const completedWithErrorStep = {
type: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR,
title: 'Authorization Error',
messages: [
{
text: 'The authorization process completed with an error.',
classList: ['error'],
},
],
links: [
{
href: 'http://client-callback?error=server_error',
rel: 'authorization-response',
},
],
error: 'server_error',
error_description: 'An error occurred during authorization',
} as HaapiCompletedWithErrorStep;

export const completedWithErrorStepWithoutLinks = {
type: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR,
title: 'Authorization Error',
messages: [
Expand Down
Loading