Skip to content

Commit 7803bfe

Browse files
committed
CCM-11492 Validate routing config ID
1 parent e6c5fb6 commit 7803bfe

File tree

9 files changed

+195
-64
lines changed

9 files changed

+195
-64
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { isValidUuid } from '@utils/is-valid-uuid';
2+
3+
describe('isValidUuid', () => {
4+
it('returns true for valid UUID v4', () => {
5+
expect(isValidUuid('a3f1c2e4-5b6d-4e8f-9a2b-1c3d4e5f6a7b')).toBe(true);
6+
expect(isValidUuid('b7e2d3c4-8f9a-4b1c-9d2e-3f4a5b6c7d8e')).toBe(true);
7+
});
8+
9+
it('returns false for invalid UUIDs', () => {
10+
expect(isValidUuid('not-a-uuid')).toBe(false);
11+
expect(isValidUuid('123456')).toBe(false);
12+
expect(isValidUuid('11111111-1111-1111-1111-111111111111')).toBe(false);
13+
});
14+
});

frontend/src/__tests__/utils/message-plans.test.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ const routingConfigApiMock = jest.mocked(routingConfigurationApiClient);
3939
const loggerMock = jest.mocked(logger);
4040
const getTemplateMock = jest.mocked(getTemplate);
4141

42+
const validRoutingConfigId = 'a3f1c2e4-5b6d-4e8f-9a2b-1c3d4e5f6a7b';
43+
const notFoundRoutingConfigId = 'b1a2c3d4-e5f6-4890-ab12-cd34ef56ab78';
44+
const invalidRoutingConfigId = 'not-a-uuid';
45+
4246
const baseConfig: RoutingConfig = {
43-
id: 'routing-config-12',
47+
id: validRoutingConfigId,
4448
name: 'Test message plan',
4549
status: 'DRAFT' as RoutingConfigStatus,
46-
clientId: 'clientemplate-1',
50+
clientId: 'client-1',
4751
campaignId: 'campaign-1',
4852
createdAt: '2025-01-01T00:00:00.000Z',
4953
updatedAt: '2025-01-01T00:00:00.000Z',
@@ -56,7 +60,7 @@ describe('@utils/message-plans', () => {
5660
jest.resetAllMocks();
5761
getSessionServerMock.mockResolvedValue({
5862
accessToken: 'mock-token',
59-
clientId: 'clientemplate-1',
63+
clientId: 'client-1',
6064
});
6165
});
6266

@@ -67,7 +71,7 @@ describe('@utils/message-plans', () => {
6771
clientId: undefined,
6872
});
6973

70-
await expect(getMessagePlan('routing-config-12')).rejects.toThrow(
74+
await expect(getMessagePlan(validRoutingConfigId)).rejects.toThrow(
7175
'Failed to get access token'
7276
);
7377

@@ -77,11 +81,11 @@ describe('@utils/message-plans', () => {
7781
it('should return the routing config on success', async () => {
7882
routingConfigApiMock.get.mockResolvedValueOnce({ data: baseConfig });
7983

80-
const response = await getMessagePlan('routing-config-12');
84+
const response = await getMessagePlan(validRoutingConfigId);
8185

8286
expect(routingConfigApiMock.get).toHaveBeenCalledWith(
8387
'mock-token',
84-
'routing-config-12'
88+
validRoutingConfigId
8589
);
8690
expect(response).toEqual(baseConfig);
8791
});
@@ -93,11 +97,11 @@ describe('@utils/message-plans', () => {
9397
},
9498
});
9599

96-
const response = await getMessagePlan('routing-config-6');
100+
const response = await getMessagePlan(notFoundRoutingConfigId);
97101

98102
expect(routingConfigApiMock.get).toHaveBeenCalledWith(
99103
'mock-token',
100-
'routing-config-6'
104+
notFoundRoutingConfigId
101105
);
102106
expect(response).toBeUndefined();
103107
expect(loggerMock.error).toHaveBeenCalledWith(
@@ -109,6 +113,12 @@ describe('@utils/message-plans', () => {
109113
})
110114
);
111115
});
116+
117+
it('should throw error for invalid routing config ID', async () => {
118+
await expect(getMessagePlan(invalidRoutingConfigId)).rejects.toThrow(
119+
'Invalid routing configuration ID'
120+
);
121+
});
112122
});
113123

114124
describe('updateMessagePlan', () => {
@@ -119,7 +129,7 @@ describe('@utils/message-plans', () => {
119129
});
120130

121131
await expect(
122-
updateMessagePlan('routing-config-12', baseConfig)
132+
updateMessagePlan(validRoutingConfigId, baseConfig)
123133
).rejects.toThrow('Failed to get access token');
124134

125135
expect(routingConfigApiMock.update).not.toHaveBeenCalled();
@@ -134,11 +144,11 @@ describe('@utils/message-plans', () => {
134144

135145
routingConfigApiMock.update.mockResolvedValueOnce({ data: updated });
136146

137-
const response = await updateMessagePlan('routing-config-12', updated);
147+
const response = await updateMessagePlan(validRoutingConfigId, updated);
138148

139149
expect(routingConfigApiMock.update).toHaveBeenCalledWith(
140150
'mock-token',
141-
'routing-config-12',
151+
validRoutingConfigId,
142152
updated
143153
);
144154
expect(response).toEqual(updated);
@@ -152,11 +162,14 @@ describe('@utils/message-plans', () => {
152162
},
153163
});
154164

155-
const response = await updateMessagePlan('routing-config-12', baseConfig);
165+
const response = await updateMessagePlan(
166+
validRoutingConfigId,
167+
baseConfig
168+
);
156169

157170
expect(routingConfigApiMock.update).toHaveBeenCalledWith(
158171
'mock-token',
159-
'routing-config-12',
172+
validRoutingConfigId,
160173
baseConfig
161174
);
162175
expect(response).toBeUndefined();
@@ -169,6 +182,12 @@ describe('@utils/message-plans', () => {
169182
})
170183
);
171184
});
185+
186+
it('should throw error for invalid routing config ID', async () => {
187+
await expect(
188+
updateMessagePlan(invalidRoutingConfigId, baseConfig)
189+
).rejects.toThrow('Invalid routing configuration ID');
190+
});
172191
});
173192

174193
describe('getMessagePlanTemplateIds', () => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function isValidUuid(id: string): boolean {
2+
return /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i.test(
3+
id
4+
);
5+
}

frontend/src/utils/message-plans.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import {
88
import { getSessionServer } from './amplify-utils';
99
import { logger } from 'nhs-notify-web-template-management-utils/logger';
1010
import { getTemplate } from './form-actions';
11+
import { isValidUuid } from './is-valid-uuid';
1112

1213
export async function getMessagePlan(
1314
routingConfigId: string
1415
): Promise<RoutingConfig | undefined> {
16+
if (!isValidUuid(routingConfigId)) {
17+
throw new Error('Invalid routing configuration ID');
18+
}
19+
1520
const { accessToken } = await getSessionServer();
1621

1722
if (!accessToken) {
@@ -36,6 +41,10 @@ export async function updateMessagePlan(
3641
routingConfigId: string,
3742
updatedMessagePlan: RoutingConfig
3843
): Promise<RoutingConfig | undefined> {
44+
if (!isValidUuid(routingConfigId)) {
45+
throw new Error('Invalid routing configuration ID');
46+
}
47+
3948
const { accessToken } = await getSessionServer();
4049

4150
if (!accessToken) {

lambdas/backend-client/src/__tests__/routing-config-api-client.test.ts

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import axios from 'axios';
22
import MockAdapter from 'axios-mock-adapter';
33
import { createAxiosClient } from '../axios-client';
4-
import { RoutingConfigurationApiClient } from '../routing-config-api-client';
4+
import {
5+
isValidUuid,
6+
RoutingConfigurationApiClient,
7+
} from '../routing-config-api-client';
58
import { RoutingConfigStatus } from '../types/generated';
9+
import { ErrorCase } from '../types/error-cases';
610

711
jest.mock('../axios-client', () => {
812
const actual = jest.requireActual('../axios-client');
@@ -14,6 +18,10 @@ jest.mock('../axios-client', () => {
1418

1519
const createAxiosClientMock = jest.mocked(createAxiosClient);
1620

21+
const validRoutingConfigId = '2a4b6c8d-0e1f-4a2b-9c3d-5e6f7a8b9c0d';
22+
const notFoundRoutingConfigId = '3b5d7f9a-1c2e-4b3d-8f0a-6e7d8c9b0a1f';
23+
const invalidRoutingConfigId = 'not-a-uuid';
24+
1725
describe('RoutingConfigurationApiClient', () => {
1826
const axiosMock = new MockAdapter(axios);
1927

@@ -24,30 +32,49 @@ describe('RoutingConfigurationApiClient', () => {
2432

2533
describe('get', () => {
2634
it('should return error when failing to fetch from API', async () => {
27-
axiosMock.onGet('/v1/routing-configuration/routing-config-1').reply(400, {
28-
statusCode: 400,
29-
technicalMessage: 'Bad request',
30-
details: { message: 'Broken' },
31-
});
35+
axiosMock
36+
.onGet(`/v1/routing-configuration/${notFoundRoutingConfigId}`)
37+
.reply(404, {
38+
statusCode: 404,
39+
technicalMessage: 'Not Found',
40+
details: { message: 'Routing configuration not found' },
41+
});
3242

3343
const client = new RoutingConfigurationApiClient();
3444

35-
const response = await client.get('mock-token', 'routing-config-1');
45+
const response = await client.get('mock-token', notFoundRoutingConfigId);
3646

3747
expect(response.error).toEqual({
3848
errorMeta: {
39-
code: 400,
40-
description: 'Bad request',
41-
details: { message: 'Broken' },
49+
code: 404,
50+
description: 'Not Found',
51+
details: { message: 'Routing configuration not found' },
4252
},
4353
});
4454
expect(response.data).toBeUndefined();
4555
expect(axiosMock.history.get.length).toBe(1);
4656
});
4757

58+
it('should return error for invalid routing config ID', async () => {
59+
const client = new RoutingConfigurationApiClient();
60+
61+
const response = await client.get('mock-token', invalidRoutingConfigId);
62+
63+
expect(response.error).toEqual({
64+
errorMeta: {
65+
code: ErrorCase.VALIDATION_FAILED,
66+
description: 'Invalid routing configuration ID format',
67+
details: { id: invalidRoutingConfigId },
68+
},
69+
actualError: undefined,
70+
});
71+
expect(response.data).toBeUndefined();
72+
expect(axiosMock.history.get.length).toBe(0);
73+
});
74+
4875
it('should return routing configuration on success', async () => {
4976
const data = {
50-
id: 'routing-config-1',
77+
id: validRoutingConfigId,
5178
name: 'Test message plan',
5279
status: 'DRAFT' as RoutingConfigStatus,
5380
clientId: 'client-1',
@@ -58,13 +85,15 @@ describe('RoutingConfigurationApiClient', () => {
5885
cascadeGroupOverrides: [],
5986
};
6087

61-
axiosMock.onGet('/v1/routing-configuration/routing-config-1').reply(200, {
62-
data,
63-
});
88+
axiosMock
89+
.onGet(`/v1/routing-configuration/${validRoutingConfigId}`)
90+
.reply(200, {
91+
data,
92+
});
6493

6594
const client = new RoutingConfigurationApiClient();
6695

67-
const response = await client.get('mock-token', 'routing-config-1');
96+
const response = await client.get('mock-token', validRoutingConfigId);
6897

6998
expect(response.error).toBeUndefined();
7099
expect(response.data).toEqual(data);
@@ -74,16 +103,18 @@ describe('RoutingConfigurationApiClient', () => {
74103

75104
describe('update', () => {
76105
it('should return error when failing to update via API', async () => {
77-
axiosMock.onPut('/v1/routing-configuration/routing-config-2').reply(400, {
78-
statusCode: 400,
79-
technicalMessage: 'Bad request',
80-
details: { message: 'Broken' },
81-
});
106+
axiosMock
107+
.onPut(`/v1/routing-configuration/${notFoundRoutingConfigId}`)
108+
.reply(404, {
109+
statusCode: 404,
110+
technicalMessage: 'Not Found',
111+
details: { message: 'Routing configuration not found' },
112+
});
82113

83114
const client = new RoutingConfigurationApiClient();
84115

85116
const body = {
86-
id: 'routing-config-2',
117+
id: notFoundRoutingConfigId,
87118
name: 'Test plan',
88119
status: 'DRAFT' as RoutingConfigStatus,
89120
clientId: 'client-1',
@@ -96,24 +127,57 @@ describe('RoutingConfigurationApiClient', () => {
96127

97128
const response = await client.update(
98129
'test-token',
99-
'routing-config-2',
130+
notFoundRoutingConfigId,
100131
body
101132
);
102133

103134
expect(response.error).toEqual({
104135
errorMeta: {
105-
code: 400,
106-
description: 'Bad request',
107-
details: { message: 'Broken' },
136+
code: 404,
137+
description: 'Not Found',
138+
details: { message: 'Routing configuration not found' },
108139
},
109140
});
110141
expect(response.data).toBeUndefined();
111142
expect(axiosMock.history.put.length).toBe(1);
112143
});
113144

145+
it('should return error for invalid routing config ID', async () => {
146+
const client = new RoutingConfigurationApiClient();
147+
148+
const body = {
149+
id: invalidRoutingConfigId,
150+
name: 'Test plan',
151+
status: 'DRAFT' as RoutingConfigStatus,
152+
clientId: 'client-1',
153+
campaignId: 'campaign-1',
154+
createdAt: '2025-01-01T00:00:00.000Z',
155+
updatedAt: '2025-01-02T00:00:00.000Z',
156+
cascade: [],
157+
cascadeGroupOverrides: [],
158+
};
159+
160+
const response = await client.update(
161+
'mock-token',
162+
invalidRoutingConfigId,
163+
body
164+
);
165+
166+
expect(response.error).toEqual({
167+
errorMeta: {
168+
code: ErrorCase.VALIDATION_FAILED,
169+
description: 'Invalid routing configuration ID format',
170+
details: { id: invalidRoutingConfigId },
171+
},
172+
actualError: undefined,
173+
});
174+
expect(response.data).toBeUndefined();
175+
expect(axiosMock.history.get.length).toBe(0);
176+
});
177+
114178
it('should return updated routing configuration on success', async () => {
115179
const body = {
116-
id: 'routing-config-2',
180+
id: '4c6e8f0a-2b3d-4c5e-9a1b-7d8c9b0a1f2e',
117181
name: 'Updated Plan',
118182
status: 'DRAFT' as RoutingConfigStatus,
119183
clientId: 'client-1',
@@ -132,7 +196,7 @@ describe('RoutingConfigurationApiClient', () => {
132196

133197
const response = await client.update(
134198
'test-token',
135-
'routing-config-2',
199+
'4c6e8f0a-2b3d-4c5e-9a1b-7d8c9b0a1f2e',
136200
body
137201
);
138202

@@ -141,4 +205,17 @@ describe('RoutingConfigurationApiClient', () => {
141205
expect(axiosMock.history.put.length).toBe(1);
142206
});
143207
});
208+
209+
describe('isValidUuid', () => {
210+
it('returns true for valid UUID v4', () => {
211+
expect(isValidUuid('a3f1c2e4-5b6d-4e8f-9a2b-1c3d4e5f6a7b')).toBe(true);
212+
expect(isValidUuid('b7e2d3c4-8f9a-4b1c-9d2e-3f4a5b6c7d8e')).toBe(true);
213+
});
214+
215+
it('returns false for invalid UUIDs', () => {
216+
expect(isValidUuid('not-a-uuid')).toBe(false);
217+
expect(isValidUuid('123456')).toBe(false);
218+
expect(isValidUuid('11111111-1111-1111-1111-111111111111')).toBe(false);
219+
});
220+
});
144221
});

0 commit comments

Comments
 (0)