Skip to content

Commit 9d92cbb

Browse files
committed
validate campaignId
1 parent ebb8c62 commit 9d92cbb

File tree

3 files changed

+215
-6
lines changed

3 files changed

+215
-6
lines changed

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

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,24 @@ import {
88
CreateUpdateRoutingConfig,
99
RoutingConfig,
1010
} from 'nhs-notify-backend-client';
11+
import { ClientConfigRepository } from '@backend-api/templates/infra/client-config-repository';
1112

1213
const user = { userId: 'userid', clientId: 'nhs-notify-client-id' };
1314

1415
function setup() {
15-
const repo = mock<RoutingConfigRepository>();
16+
const routingConfigRepository = mock<RoutingConfigRepository>();
17+
18+
const clientConfigRepository = mock<ClientConfigRepository>();
1619

1720
const mocks = {
18-
routingConfigRepository: repo,
21+
routingConfigRepository,
22+
clientConfigRepository,
1923
};
2024

21-
const client = new RoutingConfigClient(repo);
25+
const client = new RoutingConfigClient(
26+
routingConfigRepository,
27+
clientConfigRepository
28+
);
2229

2330
return { client, mocks };
2431
}
@@ -242,10 +249,11 @@ describe('RoutingConfigClient', () => {
242249
const { client, mocks } = setup();
243250

244251
const date = new Date();
252+
const campaignId = 'campaign';
245253

246254
const input: CreateUpdateRoutingConfig = {
247255
name: 'rc',
248-
campaignId: 'campaign',
256+
campaignId,
249257
cascade: [
250258
{
251259
cascadeGroups: ['standard'],
@@ -266,12 +274,20 @@ describe('RoutingConfigClient', () => {
266274
updatedAt: date.toISOString(),
267275
};
268276

277+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
278+
data: { features: {}, campaignIds: [campaignId] },
279+
});
280+
269281
mocks.routingConfigRepository.create.mockResolvedValueOnce({
270282
data: rc,
271283
});
272284

273285
const result = await client.createRoutingConfig(input, user);
274286

287+
expect(mocks.clientConfigRepository.get).toHaveBeenCalledWith(
288+
user.clientId
289+
);
290+
275291
expect(mocks.routingConfigRepository.create).toHaveBeenCalledWith(
276292
input,
277293
user
@@ -339,6 +355,10 @@ describe('RoutingConfigClient', () => {
339355
cascadeGroupOverrides: [{ name: 'standard' }],
340356
};
341357

358+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
359+
data: { features: {}, campaignIds: ['campaign'] },
360+
});
361+
342362
mocks.routingConfigRepository.create.mockResolvedValueOnce({
343363
error: { errorMeta: { code: 500, description: 'ddb err' } },
344364
});
@@ -359,6 +379,81 @@ describe('RoutingConfigClient', () => {
359379
},
360380
});
361381
});
382+
383+
test('returns failures from client config repository', async () => {
384+
const { client, mocks } = setup();
385+
386+
const input: CreateUpdateRoutingConfig = {
387+
name: 'rc',
388+
campaignId: 'campaign',
389+
cascade: [
390+
{
391+
cascadeGroups: ['standard'],
392+
channel: 'SMS',
393+
channelType: 'primary',
394+
defaultTemplateId: 'sms',
395+
},
396+
],
397+
cascadeGroupOverrides: [{ name: 'standard' }],
398+
};
399+
400+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
401+
error: {
402+
errorMeta: {
403+
code: 500,
404+
description: 'could not fetch client config',
405+
},
406+
},
407+
});
408+
409+
const result = await client.createRoutingConfig(input, user);
410+
411+
expect(mocks.routingConfigRepository.create).not.toHaveBeenCalled();
412+
413+
expect(result).toEqual({
414+
error: {
415+
errorMeta: {
416+
code: 500,
417+
description: 'could not fetch client config',
418+
},
419+
},
420+
});
421+
});
422+
423+
test('returns failure if campaignId is not allowed for the client', async () => {
424+
const { client, mocks } = setup();
425+
426+
const input: CreateUpdateRoutingConfig = {
427+
name: 'rc',
428+
campaignId: 'campaign',
429+
cascade: [
430+
{
431+
cascadeGroups: ['standard'],
432+
channel: 'SMS',
433+
channelType: 'primary',
434+
defaultTemplateId: 'sms',
435+
},
436+
],
437+
cascadeGroupOverrides: [{ name: 'standard' }],
438+
};
439+
440+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
441+
data: { features: {}, campaignIds: ['another campaign'] },
442+
});
443+
444+
const result = await client.createRoutingConfig(input, user);
445+
446+
expect(mocks.routingConfigRepository.create).not.toHaveBeenCalled();
447+
448+
expect(result).toEqual({
449+
error: {
450+
errorMeta: {
451+
code: 400,
452+
description: 'Invalid campaign ID in request',
453+
},
454+
},
455+
});
456+
});
362457
});
363458

364459
describe('submitRoutingConfig', () => {
@@ -405,6 +500,10 @@ describe('RoutingConfigClient', () => {
405500
...update,
406501
};
407502

503+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
504+
data: { features: {}, campaignIds: [routingConfig.campaignId] },
505+
});
506+
408507
mocks.routingConfigRepository.update.mockResolvedValueOnce({
409508
data: updated,
410509
});
@@ -462,5 +561,74 @@ describe('RoutingConfigClient', () => {
462561
},
463562
});
464563
});
564+
565+
test('returns failures from client config repository', async () => {
566+
const { client, mocks } = setup();
567+
568+
const update: CreateUpdateRoutingConfig = {
569+
campaignId: routingConfig.campaignId,
570+
cascade: routingConfig.cascade,
571+
cascadeGroupOverrides: routingConfig.cascadeGroupOverrides,
572+
name: 'new name',
573+
};
574+
575+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
576+
error: {
577+
errorMeta: {
578+
code: 500,
579+
description: 'could not fetch client config',
580+
},
581+
},
582+
});
583+
584+
const result = await client.updateRoutingConfig(
585+
routingConfig.id,
586+
update,
587+
user
588+
);
589+
590+
expect(mocks.routingConfigRepository.update).not.toHaveBeenCalled();
591+
592+
expect(result).toEqual({
593+
error: {
594+
errorMeta: {
595+
code: 500,
596+
description: 'could not fetch client config',
597+
},
598+
},
599+
});
600+
});
601+
602+
test('returns failure if campaignId is not allowed for the client', async () => {
603+
const { client, mocks } = setup();
604+
605+
const update: CreateUpdateRoutingConfig = {
606+
cascade: routingConfig.cascade,
607+
cascadeGroupOverrides: routingConfig.cascadeGroupOverrides,
608+
name: routingConfig.name,
609+
campaignId: 'this campaign',
610+
};
611+
612+
mocks.clientConfigRepository.get.mockResolvedValueOnce({
613+
data: { features: {}, campaignIds: ['another campaign'] },
614+
});
615+
616+
const result = await client.updateRoutingConfig(
617+
routingConfig.id,
618+
update,
619+
user
620+
);
621+
622+
expect(mocks.routingConfigRepository.update).not.toHaveBeenCalled();
623+
624+
expect(result).toEqual({
625+
error: {
626+
errorMeta: {
627+
code: 400,
628+
description: 'Invalid campaign ID in request',
629+
},
630+
},
631+
});
632+
});
465633
});
466634
});

lambdas/backend-api/src/templates/app/routing-config-client.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {
1010
import { validate } from '@backend-api/utils/validate';
1111
import type { RoutingConfigRepository } from '../infra/routing-config-repository';
1212
import type { User } from 'nhs-notify-web-template-management-utils';
13+
import { ClientConfigRepository } from '../infra/client-config-repository';
1314

1415
export class RoutingConfigClient {
1516
constructor(
16-
private readonly routingConfigRepository: RoutingConfigRepository
17+
private readonly routingConfigRepository: RoutingConfigRepository,
18+
private readonly clientConfigRepository: ClientConfigRepository
1719
) {}
1820

1921
async createRoutingConfig(
@@ -27,6 +29,24 @@ export class RoutingConfigClient {
2729

2830
if (validationResult.error) return validationResult;
2931

32+
const validated = validationResult.data;
33+
34+
const clientConfigurationResult = await this.clientConfigRepository.get(
35+
user.clientId
36+
);
37+
38+
const { data: clientConfiguration, error: clientConfigurationError } =
39+
clientConfigurationResult;
40+
41+
if (clientConfigurationError) return clientConfigurationResult;
42+
43+
if (!clientConfiguration?.campaignIds?.includes(validated.campaignId)) {
44+
return failure(
45+
ErrorCase.VALIDATION_FAILED,
46+
'Invalid campaign ID in request'
47+
);
48+
}
49+
3050
return this.routingConfigRepository.create(validationResult.data, user);
3151
}
3252

@@ -42,6 +62,24 @@ export class RoutingConfigClient {
4262

4363
if (validationResult.error) return validationResult;
4464

65+
const validated = validationResult.data;
66+
67+
const clientConfigurationResult = await this.clientConfigRepository.get(
68+
user.clientId
69+
);
70+
71+
const { data: clientConfiguration, error: clientConfigurationError } =
72+
clientConfigurationResult;
73+
74+
if (clientConfigurationError) return clientConfigurationResult;
75+
76+
if (!clientConfiguration?.campaignIds?.includes(validated.campaignId)) {
77+
return failure(
78+
ErrorCase.VALIDATION_FAILED,
79+
'Invalid campaign ID in request'
80+
);
81+
}
82+
4583
return this.routingConfigRepository.update(
4684
routingConfigId,
4785
validationResult.data,

lambdas/backend-api/src/templates/container.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export function createContainer() {
7474
config.routingConfigTableName
7575
);
7676

77-
const routingConfigClient = new RoutingConfigClient(routingConfigRepository);
77+
const routingConfigClient = new RoutingConfigClient(
78+
routingConfigRepository,
79+
clientConfigRepository
80+
);
7881

7982
return {
8083
clientConfigRepository,

0 commit comments

Comments
 (0)