Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e66b229
CCM-13003 Prevent templates being deleted when part of a message plan
nicki-nhs Jan 19, 2026
13a5eb1
Merge branch 'main' of https://github.com/NHSDigital/nhs-notify-web-t…
nicki-nhs Jan 19, 2026
db56d11
CCM-13003 Update package lock
nicki-nhs Jan 19, 2026
e45829b
CCM-13003 Rerun generate deps
nicki-nhs Jan 19, 2026
e997d39
CCM-13003 Lambda infra
nicki-nhs Jan 19, 2026
f3a4344
CCM-13003 Playwright tests
nicki-nhs Jan 19, 2026
5f936fc
CCM-13003 Fix test
nicki-nhs Jan 19, 2026
5418bbc
CCM-13003 Update protected routes
nicki-nhs Jan 20, 2026
4e79c5a
CCM-13003 Give delete lambda access to routing configs
nicki-nhs Jan 20, 2026
9b3883b
CCM-13003 Update lock number tests
nicki-nhs Jan 20, 2026
8b06548
CCM-13003 Lock number unit tests
nicki-nhs Jan 20, 2026
f84f6c4
CC-13003 Update plan name locator
nicki-nhs Jan 20, 2026
a7d3099
CCM-13003 Empty commit
nicki-nhs Jan 20, 2026
005ba89
CCM-13003 Add additional log
nicki-nhs Jan 20, 2026
e9dc811
CCM-13003 Increase test timeout
nicki-nhs Jan 20, 2026
539b176
CCM-13003 Additional logs for debug
nicki-nhs Jan 20, 2026
daf1a34
CCM-13003 Fix unit test
nicki-nhs Jan 20, 2026
91f33fb
CCM-13003 Removed additional logging from code coverage
nicki-nhs Jan 20, 2026
6fa4385
Merge branch 'main' of https://github.com/NHSDigital/nhs-notify-web-t…
nicki-nhs Jan 20, 2026
e0db7c4
Merge branch 'main' of https://github.com/NHSDigital/nhs-notify-web-t…
nicki-nhs Jan 21, 2026
1ce82f1
CCM-13003 Fix merge conflict issue
nicki-nhs Jan 21, 2026
eec5304
CCM-13003 Remove additional logging
nicki-nhs Jan 21, 2026
e24593c
CCM-13003 Remove unit test
nicki-nhs Jan 21, 2026
1e44c53
Merge branch 'main' of https://github.com/NHSDigital/nhs-notify-web-t…
nicki-nhs Jan 22, 2026
abff0d0
CCM-13003 Add blocked delete test and add error code to response
nicki-nhs Jan 22, 2026
f92ce15
CCM-13003 Api test
nicki-nhs Jan 23, 2026
66e5dc3
CCM-13003 Api test
nicki-nhs Jan 23, 2026
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
@@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DeleteTemplateError page matches snapshot 1`] = `
<DocumentFragment>
<main
class="nhsuk-main-wrapper"
id="maincontent"
role="main"
>
<div
class="nhsuk-grid-row"
data-testid="page-content-wrapper"
>
<div
class="nhsuk-grid-column-two-thirds"
>
<h1
class="nhsuk-heading-l"
data-testid="page-heading"
>
You cannot delete the template 'Test Template'
</h1>
<p>
The template is linked to these message plans:
</p>
<ul
class="nhsuk-list nhsuk-list--bullet"
data-testid="message-plan-list"
>
<li>
Email Campaign
</li>
<li>
SMS Notification
</li>
</ul>
<p>
You need to unlink it from each message plan before you can delete it.
</p>
<p>
<a
data-testid="back-link-bottom"
href="/message-templates"
>
Go back
</a>
</p>
</div>
</div>
</main>
</DocumentFragment>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { render } from '@testing-library/react';
import DeleteTemplateErrorPage, {
generateMetadata,
} from '@app/delete-template-error/[templateId]/page';
import { getTemplate } from '@utils/form-actions';
import { getRoutingConfigReferencesByTemplateId } from '@utils/message-plans';
import type {
TemplateDto,
RoutingConfigReference,
} from 'nhs-notify-backend-client';
import { redirect } from 'next/navigation';
import { NextRedirectError } from '@testhelpers/next-redirect';
import DeleteTemplateError from '@molecules/DeleteTemplateError/DeleteTemplateError';

jest.mock('next/navigation', () => ({
redirect: jest.fn(),
RedirectType: { replace: 'replace', push: 'push' },
}));

jest.mocked(redirect).mockImplementation((url, type) => {
throw new NextRedirectError(url, type);
});

jest.mock('@utils/form-actions', () => ({
getTemplate: jest.fn(),
}));

jest.mock('@utils/message-plans', () => ({
getRoutingConfigReferencesByTemplateId: jest.fn(),
}));

const redirectMock = jest.mocked(redirect);
const getTemplateMock = jest.mocked(getTemplate);
const getRoutingConfigReferencesByTemplateIdMock = jest.mocked(
getRoutingConfigReferencesByTemplateId
);

const mockTemplate: Extract<TemplateDto, { templateType: 'NHS_APP' }> = {
id: 'template-123',
name: 'Test Template',
templateType: 'NHS_APP',
templateStatus: 'NOT_YET_SUBMITTED',
lockNumber: 1,
message: 'Test message',
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
};

describe('DeleteTemplateError page', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('has correct page title', async () => {
const metadata = await generateMetadata();

expect(metadata).toEqual({
title: 'Delete template error - NHS Notify',
});
});

test('redirects when template is not found', async () => {
getTemplateMock.mockResolvedValueOnce(undefined);

await expect(
DeleteTemplateErrorPage({
params: Promise.resolve({ templateId: 'non-existent-template' }),
})
).rejects.toThrow(NextRedirectError);

expect(getTemplateMock).toHaveBeenCalledWith('non-existent-template');
expect(getRoutingConfigReferencesByTemplateIdMock).not.toHaveBeenCalled();
});

test('redirects when no message plans are linked to template', async () => {
getTemplateMock.mockResolvedValueOnce(mockTemplate);
getRoutingConfigReferencesByTemplateIdMock.mockResolvedValueOnce([]);

await expect(
DeleteTemplateErrorPage({
params: Promise.resolve({ templateId: 'template-123' }),
})
).rejects.toThrow(NextRedirectError);

expect(getTemplateMock).toHaveBeenCalledWith('template-123');
expect(getRoutingConfigReferencesByTemplateIdMock).toHaveBeenCalledWith(
'template-123'
);
});

test('displays error page when message plans are linked to template', async () => {
const messagePlans: RoutingConfigReference[] = [
{ id: '90e46ece-4a3b-47bd-b781-f986b42a5a09', name: 'Message Plan 1' },
{ id: 'a0e46ece-4a3b-47bd-b781-f986b42a5a10', name: 'Message Plan 2' },
];

getTemplateMock.mockResolvedValueOnce(mockTemplate);
getRoutingConfigReferencesByTemplateIdMock.mockResolvedValueOnce(
messagePlans
);

const result = await DeleteTemplateErrorPage({
params: Promise.resolve({ templateId: 'template-123' }),
});

expect(getTemplateMock).toHaveBeenCalledWith('template-123');
expect(getRoutingConfigReferencesByTemplateIdMock).toHaveBeenCalledWith(
'template-123'
);
expect(redirectMock).not.toHaveBeenCalled();

expect(result).toEqual(
<DeleteTemplateError
templateName='Test Template'
messagePlans={messagePlans}
/>
);
});

test('matches snapshot', async () => {
const messagePlans: RoutingConfigReference[] = [
{ id: 'plan-1', name: 'Email Campaign' },
{ id: 'plan-2', name: 'SMS Notification' },
];

getTemplateMock.mockResolvedValueOnce(mockTemplate);
getRoutingConfigReferencesByTemplateIdMock.mockResolvedValueOnce(
messagePlans
);

const page = await DeleteTemplateErrorPage({
params: Promise.resolve({ templateId: 'template-789' }),
});

expect(render(page).asFragment()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ import { setTemplateToDeleted } from '@utils/form-actions';

jest.mock('next/navigation');
jest.mock('@utils/form-actions');
jest.mock('nhs-notify-web-template-management-utils/logger', () => ({
logger: {
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
},
}));

beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2022-01-01 09:00'));
});

beforeEach(() => {
jest.clearAllMocks();
});

test('redirects', async () => {
const mockRedirect = jest.mocked(redirect);

Expand Down Expand Up @@ -49,3 +61,55 @@ test('calls form action and redirects', async () => {
RedirectType.push
);
});

test('redirects to error page when template is linked to message plans', async () => {
const mockSetTemplateToDeleted = jest.mocked(setTemplateToDeleted);
const mockRedirect = jest.mocked(redirect);

mockSetTemplateToDeleted.mockRejectedValueOnce(new Error('TEMPLATE_IN_USE'));

const mockTemplate: NHSAppTemplate = {
id: 'template-id',
name: 'template-name',
message: 'template-message',
templateType: 'NHS_APP',
templateStatus: 'NOT_YET_SUBMITTED',
createdAt: 'today',
updatedAt: 'today',
lockNumber: 1,
};

await deleteTemplateYesAction(mockTemplate);

expect(mockSetTemplateToDeleted).toHaveBeenCalledWith('template-id', 1);
expect(mockRedirect).toHaveBeenCalledWith(
'/delete-template-error/template-id',
RedirectType.push
);
});

test('rethrows other errors', async () => {
const mockSetTemplateToDeleted = jest.mocked(setTemplateToDeleted);
const mockRedirect = jest.mocked(redirect);

const unexpectedError = new Error('Database connection failed');
mockSetTemplateToDeleted.mockRejectedValueOnce(unexpectedError);

const mockTemplate: NHSAppTemplate = {
id: 'template-id',
name: 'template-name',
message: 'template-message',
templateType: 'NHS_APP',
templateStatus: 'NOT_YET_SUBMITTED',
createdAt: 'today',
updatedAt: 'today',
lockNumber: 1,
};

await expect(deleteTemplateYesAction(mockTemplate)).rejects.toThrow(
'Database connection failed'
);

expect(mockSetTemplateToDeleted).toHaveBeenCalledWith('template-id', 1);
expect(mockRedirect).not.toHaveBeenCalled();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen } from '@testing-library/react';
import DeleteTemplateError from '@molecules/DeleteTemplateError/DeleteTemplateError';
import type { RoutingConfigReference } from 'nhs-notify-backend-client';

describe('DeleteTemplateError component', () => {
test('renders template name in heading', () => {
const messagePlans: RoutingConfigReference[] = [
{ id: '90e46ece-4a3b-47bd-b781-f986b42a5a09', name: 'Test Message Plan' },
];

render(
<DeleteTemplateError
templateName='Test Template'
messagePlans={messagePlans}
/>
);

expect(
screen.getByText(/You cannot delete the template/)
).toBeInTheDocument();

expect(screen.getByText(/'Test Template'/)).toBeInTheDocument();
});

test('renders single message plan', () => {
const messagePlans: RoutingConfigReference[] = [
{ id: '90e46ece-4a3b-47bd-b781-f986b42a5a09', name: 'Single Plan' },
];

render(
<DeleteTemplateError
templateName='My Template'
messagePlans={messagePlans}
/>
);

expect(screen.getByText('Single Plan')).toBeInTheDocument();
});

test('renders multiple message plans', () => {
const messagePlans: RoutingConfigReference[] = [
{ id: '90e46ece-4a3b-47bd-b781-f986b42a5a09', name: 'Message Plan 1' },
{ id: 'a0e46ece-4a3b-47bd-b781-f986b42a5a10', name: 'Message Plan 2' },
{ id: 'b0e46ece-4a3b-47bd-b781-f986b42a5a11', name: 'Message Plan 3' },
];

const { container } = render(
<DeleteTemplateError
templateName='Test Template'
messagePlans={messagePlans}
/>
);

expect(screen.getByText('Message Plan 1')).toBeInTheDocument();
expect(screen.getByText('Message Plan 2')).toBeInTheDocument();
expect(screen.getByText('Message Plan 3')).toBeInTheDocument();

const listItems = container.querySelectorAll('li');
expect(listItems.length).toBe(3);
});

test('renders back link', () => {
const messagePlans: RoutingConfigReference[] = [
{ id: 'plan-1', name: 'Test Plan' },
];

render(
<DeleteTemplateError
templateName='Test Template'
messagePlans={messagePlans}
/>
);

const backLink = screen.getByTestId('back-link-bottom');
expect(backLink).toBeInTheDocument();
expect(backLink).toHaveAttribute('href', '/message-templates');
});

test('matches snapshot', () => {
const messagePlans: RoutingConfigReference[] = [
{ id: '90e46ece-4a3b-47bd-b781-f986b42a5a09', name: 'Flu Campaign' },
{ id: 'a0e46ece-4a3b-47bd-b781-f986b42a5a10', name: 'Covid Campaign' },
];

const { asFragment } = render(
<DeleteTemplateError
templateName='Test Template'
messagePlans={messagePlans}
/>
);

expect(asFragment()).toMatchSnapshot();
});
});
Loading