Skip to content

Commit d2ec376

Browse files
committed
CCM-11148: hook up count and list API routes for Message plans --no-verify
1 parent a452cac commit d2ec376

File tree

11 files changed

+417
-153
lines changed

11 files changed

+417
-153
lines changed
Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @jest-environment node
33
*/
4-
import { getRoutingConfigs } from '@utils/form-actions';
4+
import { getRoutingConfigs, countRoutingConfigs } from '@utils/form-actions';
55
import {
66
type MessagePlansProps,
77
MessagePlans,
@@ -10,15 +10,30 @@ import { serverIsFeatureEnabled } from '@utils/server-features';
1010
import MessagePlansPage, { generateMetadata } from '@app/message-plans/page';
1111
import { redirect } from 'next/navigation';
1212
import { ReactElement } from 'react';
13+
import { RoutingConfig } from 'nhs-notify-backend-client';
1314

1415
jest.mock('next/navigation');
1516
jest.mock('@utils/form-actions');
1617
jest.mock('@utils/server-features');
1718

19+
const countRoutingConfigsMock = jest.mocked(countRoutingConfigs);
1820
const serverIsFeatureEnabledMock = jest.mocked(serverIsFeatureEnabled);
1921
const getRoutingConfigsMock = jest.mocked(getRoutingConfigs);
2022
const redirectMock = jest.mocked(redirect);
2123

24+
const buildRoutingConfig = (rc: Partial<RoutingConfig>): RoutingConfig => ({
25+
campaignId: 'abc',
26+
cascade: [],
27+
cascadeGroupOverrides: [],
28+
clientId: 'client-a',
29+
createdAt: '2025-09-09T10:00:00Z',
30+
status: 'DRAFT',
31+
id: '',
32+
name: '',
33+
updatedAt: '',
34+
...rc,
35+
});
36+
2237
describe('MessagePlansPage', () => {
2338
beforeEach(() => {
2439
jest.clearAllMocks();
@@ -44,26 +59,28 @@ describe('MessagePlansPage', () => {
4459

4560
test('renders the page with drafts and production routing plans', async () => {
4661
getRoutingConfigsMock.mockResolvedValueOnce([
47-
{
62+
buildRoutingConfig({
4863
id: '1',
4964
name: 'Draft A',
50-
lastUpdated: '2025-09-10T10:00:00Z',
65+
updatedAt: '2025-09-10T10:00:00Z',
5166
status: 'DRAFT',
52-
},
53-
{
67+
}),
68+
buildRoutingConfig({
5469
id: '2',
5570
name: 'Prod X',
56-
lastUpdated: '2025-09-09T11:00:00Z',
57-
status: 'PRODUCTION',
58-
},
59-
{
71+
updatedAt: '2025-09-09T11:00:00Z',
72+
status: 'COMPLETED',
73+
}),
74+
buildRoutingConfig({
6075
id: '3',
6176
name: 'Prod Y',
62-
lastUpdated: '2025-09-08T12:00:00Z',
63-
status: 'PRODUCTION',
64-
},
77+
updatedAt: '2025-09-08T12:00:00Z',
78+
status: 'COMPLETED',
79+
}),
6580
]);
6681

82+
countRoutingConfigsMock.mockResolvedValueOnce(1).mockResolvedValueOnce(2);
83+
6784
const page = (await MessagePlansPage()) as ReactElement<
6885
MessagePlansProps,
6986
typeof MessagePlans
@@ -75,6 +92,10 @@ describe('MessagePlansPage', () => {
7592

7693
expect(getRoutingConfigsMock).toHaveBeenCalledTimes(1);
7794

95+
expect(countRoutingConfigsMock).toHaveBeenNthCalledWith(1, 'DRAFT');
96+
97+
expect(countRoutingConfigsMock).toHaveBeenNthCalledWith(2, 'COMPLETED');
98+
7899
expect(page.props.draft).toEqual({
79100
plans: [
80101
{
@@ -93,37 +114,16 @@ describe('MessagePlansPage', () => {
93114
id: '2',
94115
name: 'Prod X',
95116
lastUpdated: '2025-09-09T11:00:00Z',
96-
status: 'PRODUCTION',
117+
status: 'COMPLETED',
97118
},
98119
{
99120
id: '3',
100121
name: 'Prod Y',
101122
lastUpdated: '2025-09-08T12:00:00Z',
102-
status: 'PRODUCTION',
123+
status: 'COMPLETED',
103124
},
104125
],
105126
count: 2,
106127
});
107128
});
108-
109-
test('renders the page with no items', async () => {
110-
getRoutingConfigsMock.mockResolvedValueOnce([]);
111-
112-
const page = (await MessagePlansPage()) as ReactElement<
113-
MessagePlansProps,
114-
typeof MessagePlans
115-
>;
116-
117-
expect(page.props).toBeDefined();
118-
119-
expect(page.props.draft).toEqual({
120-
plans: [],
121-
count: 0,
122-
});
123-
124-
expect(page.props.production).toEqual({
125-
plans: [],
126-
count: 0,
127-
});
128-
});
129129
});

frontend/src/__tests__/components/molecules/__snapshots__/MessagePlans.test.tsx.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ exports[`MessagePlans matches snapshot 1`] = `
4646
rel="noopener noreferrer"
4747
target="_blank"
4848
>
49-
API integration environment
49+
API integration environment (opens in a new tab)
5050
</a>
5151
</li>
5252
<li>
@@ -55,7 +55,7 @@ exports[`MessagePlans matches snapshot 1`] = `
5555
rel="noopener noreferrer"
5656
target="_blank"
5757
>
58-
Integration MESH mailbox
58+
Integration MESH mailbox (opens in a new tab)
5959
</a>
6060
</li>
6161
</ul>
@@ -71,15 +71,15 @@ exports[`MessagePlans matches snapshot 1`] = `
7171
rel="noopener noreferrer"
7272
target="_blank"
7373
>
74-
NHS Notify API
74+
NHS Notify API (opens in a new tab)
7575
</a>
7676
or
7777
<a
7878
href="https://digital.nhs.uk/developer/api-catalogue/nhs-notify-mesh/"
7979
rel="noopener noreferrer"
8080
target="_blank"
8181
>
82-
NHS Notify MESH.
82+
NHS Notify MESH (opens in a new tab).
8383
</a>
8484
</p>
8585
</div>

frontend/src/__tests__/components/molecules/__snapshots__/MessagePlansList.test.tsx.snap

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -128,34 +128,9 @@ exports[`MessagePlansList matches snapshot when no data is available 1`] = `
128128
<div
129129
class="nhsuk-details__text"
130130
>
131-
<table
132-
class="nhsuk-table-responsive"
133-
>
134-
<thead
135-
class="nhsuk-table__head"
136-
role="rowgroup"
137-
/>
138-
<tbody
139-
class="nhsuk-table__body"
140-
>
141-
<tr
142-
class="nhsuk-table__row"
143-
>
144-
<td
145-
class="nhsuk-table__cell"
146-
role="cell"
147-
>
148-
<span
149-
aria-hidden="true"
150-
class="nhsuk-table-responsive__heading"
151-
>
152-
153-
</span>
154-
You do not have any message plans in draft yet.
155-
</td>
156-
</tr>
157-
</tbody>
158-
</table>
131+
<p>
132+
You do not have any message plans in draft yet.
133+
</p>
159134
</div>
160135
</details>
161136
</DocumentFragment>

frontend/src/__tests__/utils/form-actions.test.ts

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,25 @@ import {
1616
setTemplateToSubmitted,
1717
requestTemplateProof,
1818
getRoutingConfigs,
19+
countRoutingConfigs,
1920
} from '@utils/form-actions';
2021
import { getSessionServer } from '@utils/amplify-utils';
21-
import { TemplateDto } from 'nhs-notify-backend-client';
22+
import {
23+
RoutingConfig,
24+
TemplateDto,
25+
routingConfigurationApiClient,
26+
} from 'nhs-notify-backend-client';
2227
import { templateApiClient } from 'nhs-notify-backend-client/src/template-api-client';
2328

2429
const mockedTemplateClient = jest.mocked(templateApiClient);
2530
const authIdTokenServerMock = jest.mocked(getSessionServer);
31+
const routingConfigurationApiClientMock = jest.mocked(
32+
routingConfigurationApiClient
33+
);
2634

2735
jest.mock('@utils/amplify-utils');
2836
jest.mock('nhs-notify-backend-client/src/template-api-client');
37+
jest.mock('nhs-notify-backend-client/src/routing-config-api-client');
2938

3039
describe('form-actions', () => {
3140
beforeEach(() => {
@@ -708,29 +717,114 @@ describe('form-actions', () => {
708717
);
709718
});
710719

711-
test('should return a list of routing configs', async () => {
712-
const configs = await getRoutingConfigs();
713-
714-
expect(configs).toEqual([
715-
{
716-
name: 'Static routing plan 1',
717-
id: expect.anything(),
718-
lastUpdated: new Date().toString(),
719-
status: 'DRAFT',
720-
},
721-
{
722-
name: 'Static routing plan 2',
723-
id: expect.anything(),
724-
lastUpdated: new Date().toString(),
725-
status: 'DRAFT',
720+
test('should return empty array when calling the API fails', async () => {
721+
routingConfigurationApiClientMock.list.mockResolvedValueOnce({
722+
error: {
723+
errorMeta: { code: 400, description: 'Bad request' },
726724
},
727-
{
728-
name: 'Static routing plan 3',
729-
id: expect.anything(),
730-
lastUpdated: new Date('2025-09-14').toString(),
731-
status: 'DRAFT',
725+
});
726+
727+
const response = await getRoutingConfigs();
728+
729+
expect(response.length).toBe(0);
730+
});
731+
732+
test('should return a list of routing configs - order by createdAt and then id', async () => {
733+
const fields = {
734+
status: 'DRAFT',
735+
name: 'Routing config',
736+
updatedAt: '2021-01-01T00:00:00.000Z',
737+
campaignId: 'campaignId',
738+
clientId: 'clientId',
739+
cascade: [],
740+
cascadeGroupOverrides: [],
741+
} satisfies Partial<RoutingConfig>;
742+
743+
const routingConfigs = [
744+
{ ...fields, id: '06', createdAt: '2022-01-01T00:00:00.000Z' },
745+
{ ...fields, id: '08', createdAt: '2020-01-01T00:00:00.000Z' },
746+
{ ...fields, id: '05', createdAt: '2021-01-01T00:00:00.000Z' },
747+
{ ...fields, id: '02', createdAt: '2021-01-01T00:00:00.000Z' },
748+
{ ...fields, id: '01', createdAt: '2021-01-01T00:00:00.000Z' },
749+
{ ...fields, id: '03', createdAt: '2021-01-01T00:00:00.000Z' },
750+
{ ...fields, id: '04', createdAt: '2021-01-01T00:00:00.000Z' },
751+
];
752+
753+
// 06 is the newest, 08 is the oldest.
754+
// 01 - 05 all have the same createdAt.
755+
const expectedOrder = ['06', '01', '02', '03', '04', '05', '08'];
756+
757+
routingConfigurationApiClientMock.list.mockResolvedValueOnce({
758+
data: routingConfigs,
759+
});
760+
761+
const response = await getRoutingConfigs();
762+
763+
const actualOrder = [];
764+
for (const routingConfig of response) {
765+
actualOrder.push(routingConfig.id);
766+
}
767+
768+
expect(actualOrder).toEqual(expectedOrder);
769+
});
770+
});
771+
772+
describe('countRoutingConfigs', () => {
773+
test('should throw error when no token', async () => {
774+
authIdTokenServerMock.mockReset();
775+
authIdTokenServerMock.mockResolvedValueOnce({
776+
accessToken: undefined,
777+
clientId: undefined,
778+
});
779+
780+
await expect(countRoutingConfigs('DRAFT')).rejects.toThrow(
781+
'Failed to get access token'
782+
);
783+
});
784+
785+
test('should return 0 when calling the API fails', async () => {
786+
routingConfigurationApiClientMock.count.mockResolvedValueOnce({
787+
error: {
788+
errorMeta: { code: 400, description: 'Bad request' },
732789
},
733-
]);
790+
});
791+
792+
const response = await countRoutingConfigs('DRAFT');
793+
794+
expect(response).toBe(0);
795+
});
796+
797+
test('should return count of routing configurations for status', async () => {
798+
// Note: we're doing this here because we call `getSessionServer` twice
799+
// and it's only mocked-out once by default.
800+
authIdTokenServerMock.mockResolvedValue({
801+
accessToken: 'token',
802+
clientId: 'client1',
803+
});
804+
805+
routingConfigurationApiClientMock.count
806+
.mockResolvedValueOnce({
807+
data: { count: 1 },
808+
})
809+
.mockResolvedValueOnce({
810+
data: { count: 5 },
811+
});
812+
813+
const draftCount = await countRoutingConfigs('DRAFT');
814+
const completedCount = await countRoutingConfigs('COMPLETED');
815+
816+
expect(draftCount).toEqual(1);
817+
expect(routingConfigurationApiClientMock.count).toHaveBeenNthCalledWith(
818+
1,
819+
'token',
820+
'DRAFT'
821+
);
822+
expect(completedCount).toEqual(5);
823+
expect(routingConfigurationApiClientMock.count).toHaveBeenNthCalledWith(
824+
2,
825+
'token',
826+
'COMPLETED'
827+
);
734828
});
735829
});
736830
});

0 commit comments

Comments
 (0)