cdk-amazon-connect-constructs 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +10995 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/connect-constructs.test.ts +797 -0
- package/lib/connect-instance.d.ts +125 -0
- package/lib/connect-instance.d.ts.map +1 -0
- package/lib/connect-instance.js +88 -0
- package/lib/contact-flow.d.ts +81 -0
- package/lib/contact-flow.d.ts.map +1 -0
- package/lib/contact-flow.js +80 -0
- package/lib/hours-of-operation.d.ts +102 -0
- package/lib/hours-of-operation.d.ts.map +1 -0
- package/lib/hours-of-operation.js +94 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +23 -0
- package/lib/lambda-integration.d.ts +27 -0
- package/lib/lambda-integration.d.ts.map +1 -0
- package/lib/lambda-integration.js +37 -0
- package/lib/private/arn.d.ts +3 -0
- package/lib/private/arn.d.ts.map +1 -0
- package/lib/private/arn.js +8 -0
- package/lib/queue.d.ts +103 -0
- package/lib/queue.d.ts.map +1 -0
- package/lib/queue.js +64 -0
- package/lib/routing-profile.d.ts +110 -0
- package/lib/routing-profile.d.ts.map +1 -0
- package/lib/routing-profile.js +89 -0
- package/package.json +73 -0
- package/scripts/patch-java-pom.js +45 -0
- package/src/connect-instance.ts +190 -0
- package/src/contact-flow.ts +123 -0
- package/src/hours-of-operation.ts +184 -0
- package/src/index.ts +6 -0
- package/src/lambda-integration.ts +51 -0
- package/src/private/arn.ts +6 -0
- package/src/queue.ts +152 -0
- package/src/routing-profile.ts +196 -0
- package/tsconfig.jsii.json +40 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { App, Stack, aws_lambda as lambda } from 'aws-cdk-lib';
|
|
2
|
+
import { Match, Template } from 'aws-cdk-lib/assertions';
|
|
3
|
+
import {
|
|
4
|
+
Channel,
|
|
5
|
+
ConnectInstance,
|
|
6
|
+
ContactFlow,
|
|
7
|
+
ContactFlowState,
|
|
8
|
+
ContactFlowType,
|
|
9
|
+
DayOfWeek,
|
|
10
|
+
HoursOfOperation,
|
|
11
|
+
IdentityManagementType,
|
|
12
|
+
LambdaIntegration,
|
|
13
|
+
Queue,
|
|
14
|
+
QueueStatus,
|
|
15
|
+
RoutingProfile,
|
|
16
|
+
} from '../src';
|
|
17
|
+
|
|
18
|
+
function createInstance(stack: Stack): ConnectInstance {
|
|
19
|
+
return new ConnectInstance(stack, 'Instance', {
|
|
20
|
+
instanceAlias: 'support',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createBusinessHours(stack: Stack, instance: ConnectInstance): HoursOfOperation {
|
|
25
|
+
return new HoursOfOperation(stack, 'Hours', {
|
|
26
|
+
instance,
|
|
27
|
+
name: 'Business hours',
|
|
28
|
+
timeZone: 'America/New_York',
|
|
29
|
+
config: [
|
|
30
|
+
{
|
|
31
|
+
day: DayOfWeek.MONDAY,
|
|
32
|
+
startTime: { hours: 9 },
|
|
33
|
+
endTime: { hours: 17, minutes: 30 },
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createQueue(stack: Stack, instance: ConnectInstance, hours: HoursOfOperation): Queue {
|
|
40
|
+
return new Queue(stack, 'Queue', {
|
|
41
|
+
instance,
|
|
42
|
+
hoursOfOperation: hours,
|
|
43
|
+
name: 'Support',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('ConnectInstance', () => {
|
|
48
|
+
test('creates CfnInstance with default telephony attributes', () => {
|
|
49
|
+
const stack = new Stack();
|
|
50
|
+
|
|
51
|
+
new ConnectInstance(stack, 'Instance', {
|
|
52
|
+
instanceAlias: 'support',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Instance', {
|
|
56
|
+
Attributes: {
|
|
57
|
+
InboundCalls: true,
|
|
58
|
+
OutboundCalls: true,
|
|
59
|
+
ContactflowLogs: false,
|
|
60
|
+
ContactLens: false,
|
|
61
|
+
EarlyMedia: false,
|
|
62
|
+
UseCustomTTSVoices: false,
|
|
63
|
+
},
|
|
64
|
+
IdentityManagementType: 'CONNECT_MANAGED',
|
|
65
|
+
InstanceAlias: 'support',
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('passes directory id for an existing-directory instance', () => {
|
|
70
|
+
const stack = new Stack();
|
|
71
|
+
|
|
72
|
+
new ConnectInstance(stack, 'Instance', {
|
|
73
|
+
directoryId: 'd-1234567890',
|
|
74
|
+
identityManagementType: IdentityManagementType.EXISTING_DIRECTORY,
|
|
75
|
+
instanceAlias: 'support-directory',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Instance', {
|
|
79
|
+
DirectoryId: 'd-1234567890',
|
|
80
|
+
IdentityManagementType: 'EXISTING_DIRECTORY',
|
|
81
|
+
InstanceAlias: 'support-directory',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('rejects an empty instance alias', () => {
|
|
86
|
+
const stack = new Stack();
|
|
87
|
+
|
|
88
|
+
expect(() => new ConnectInstance(stack, 'Instance', {
|
|
89
|
+
instanceAlias: ' ',
|
|
90
|
+
})).toThrow(/instanceAlias/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('requires directoryId for existing-directory identity management', () => {
|
|
94
|
+
const stack = new Stack();
|
|
95
|
+
|
|
96
|
+
expect(() => new ConnectInstance(stack, 'Instance', {
|
|
97
|
+
identityManagementType: IdentityManagementType.EXISTING_DIRECTORY,
|
|
98
|
+
instanceAlias: 'support',
|
|
99
|
+
})).toThrow(/directoryId/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('imports an existing instance by ARN', () => {
|
|
103
|
+
const stack = new Stack();
|
|
104
|
+
|
|
105
|
+
const instance = ConnectInstance.fromInstanceArn(
|
|
106
|
+
stack,
|
|
107
|
+
'ImportedInstance',
|
|
108
|
+
'arn:aws:connect:us-east-1:123456789012:instance/abc123',
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(instance.instanceArn).toBe('arn:aws:connect:us-east-1:123456789012:instance/abc123');
|
|
112
|
+
expect(instance.instanceId).toBe('abc123');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('falls back to the full ARN when an imported ARN has no resource name', () => {
|
|
116
|
+
const stack = new Stack();
|
|
117
|
+
|
|
118
|
+
const instance = ConnectInstance.fromInstanceArn(
|
|
119
|
+
stack,
|
|
120
|
+
'ImportedInstance',
|
|
121
|
+
'arn:aws:connect:us-east-1:123456789012:instance',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(instance.instanceId).toBe('arn:aws:connect:us-east-1:123456789012:instance');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('passes explicit attributes through to CfnInstance', () => {
|
|
128
|
+
const stack = new Stack();
|
|
129
|
+
|
|
130
|
+
new ConnectInstance(stack, 'Instance', {
|
|
131
|
+
instanceAlias: 'support',
|
|
132
|
+
attributes: {
|
|
133
|
+
inboundCalls: false,
|
|
134
|
+
outboundCalls: false,
|
|
135
|
+
contactflowLogs: true,
|
|
136
|
+
contactLens: true,
|
|
137
|
+
earlyMedia: true,
|
|
138
|
+
useCustomTtsVoices: true,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Instance', {
|
|
143
|
+
Attributes: {
|
|
144
|
+
InboundCalls: false,
|
|
145
|
+
OutboundCalls: false,
|
|
146
|
+
ContactflowLogs: true,
|
|
147
|
+
ContactLens: true,
|
|
148
|
+
EarlyMedia: true,
|
|
149
|
+
UseCustomTTSVoices: true,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('HoursOfOperation', () => {
|
|
156
|
+
test('creates CfnHoursOfOperation with weekly ranges and instance reference', () => {
|
|
157
|
+
const stack = new Stack();
|
|
158
|
+
const instance = createInstance(stack);
|
|
159
|
+
|
|
160
|
+
createBusinessHours(stack, instance);
|
|
161
|
+
|
|
162
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::HoursOfOperation', {
|
|
163
|
+
Config: [
|
|
164
|
+
{
|
|
165
|
+
Day: 'MONDAY',
|
|
166
|
+
StartTime: {
|
|
167
|
+
Hours: 9,
|
|
168
|
+
Minutes: 0,
|
|
169
|
+
},
|
|
170
|
+
EndTime: {
|
|
171
|
+
Hours: 17,
|
|
172
|
+
Minutes: 30,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
InstanceArn: Match.anyValue(),
|
|
177
|
+
Name: 'Business hours',
|
|
178
|
+
TimeZone: 'America/New_York',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('rejects an empty config', () => {
|
|
183
|
+
const stack = new Stack();
|
|
184
|
+
const instance = createInstance(stack);
|
|
185
|
+
|
|
186
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
187
|
+
instance,
|
|
188
|
+
name: 'Business hours',
|
|
189
|
+
timeZone: 'America/New_York',
|
|
190
|
+
config: [],
|
|
191
|
+
})).toThrow(/at least one weekly time range/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('rejects an empty name', () => {
|
|
195
|
+
const stack = new Stack();
|
|
196
|
+
const instance = createInstance(stack);
|
|
197
|
+
|
|
198
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
199
|
+
instance,
|
|
200
|
+
name: ' ',
|
|
201
|
+
timeZone: 'America/New_York',
|
|
202
|
+
config: [
|
|
203
|
+
{
|
|
204
|
+
day: DayOfWeek.MONDAY,
|
|
205
|
+
startTime: { hours: 9 },
|
|
206
|
+
endTime: { hours: 17 },
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
})).toThrow(/name/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('rejects an empty time zone', () => {
|
|
213
|
+
const stack = new Stack();
|
|
214
|
+
const instance = createInstance(stack);
|
|
215
|
+
|
|
216
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
217
|
+
instance,
|
|
218
|
+
name: 'Business hours',
|
|
219
|
+
timeZone: ' ',
|
|
220
|
+
config: [
|
|
221
|
+
{
|
|
222
|
+
day: DayOfWeek.MONDAY,
|
|
223
|
+
startTime: { hours: 9 },
|
|
224
|
+
endTime: { hours: 17 },
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
})).toThrow(/timeZone/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('rejects invalid time values', () => {
|
|
231
|
+
const stack = new Stack();
|
|
232
|
+
const instance = createInstance(stack);
|
|
233
|
+
|
|
234
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
235
|
+
instance,
|
|
236
|
+
name: 'Bad hours',
|
|
237
|
+
timeZone: 'America/New_York',
|
|
238
|
+
config: [
|
|
239
|
+
{
|
|
240
|
+
day: DayOfWeek.MONDAY,
|
|
241
|
+
startTime: { hours: 24 },
|
|
242
|
+
endTime: { hours: 17 },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
})).toThrow(/startTime\.hours/);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('rejects invalid minute values', () => {
|
|
249
|
+
const stack = new Stack();
|
|
250
|
+
const instance = createInstance(stack);
|
|
251
|
+
|
|
252
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
253
|
+
instance,
|
|
254
|
+
name: 'Bad hours',
|
|
255
|
+
timeZone: 'America/New_York',
|
|
256
|
+
config: [
|
|
257
|
+
{
|
|
258
|
+
day: DayOfWeek.MONDAY,
|
|
259
|
+
startTime: { hours: 9, minutes: 60 },
|
|
260
|
+
endTime: { hours: 17 },
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
})).toThrow(/startTime\.minutes/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('rejects invalid endTime values', () => {
|
|
267
|
+
const stack = new Stack();
|
|
268
|
+
const instance = createInstance(stack);
|
|
269
|
+
|
|
270
|
+
expect(() => new HoursOfOperation(stack, 'Hours', {
|
|
271
|
+
instance,
|
|
272
|
+
name: 'Bad hours',
|
|
273
|
+
timeZone: 'America/New_York',
|
|
274
|
+
config: [
|
|
275
|
+
{
|
|
276
|
+
day: DayOfWeek.MONDAY,
|
|
277
|
+
startTime: { hours: 9 },
|
|
278
|
+
endTime: { hours: 17, minutes: 30.5 },
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
})).toThrow(/endTime\.minutes/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('imports existing hours of operation by ARN', () => {
|
|
285
|
+
const stack = new Stack();
|
|
286
|
+
|
|
287
|
+
const hours = HoursOfOperation.fromHoursOfOperationArn(
|
|
288
|
+
stack,
|
|
289
|
+
'ImportedHours',
|
|
290
|
+
'arn:aws:connect:us-east-1:123456789012:instance/abc123/operating-hours/hours123',
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(hours.hoursOfOperationArn).toBe(
|
|
294
|
+
'arn:aws:connect:us-east-1:123456789012:instance/abc123/operating-hours/hours123',
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('Queue', () => {
|
|
300
|
+
test('creates CfnQueue with correct hours of operation reference', () => {
|
|
301
|
+
const stack = new Stack();
|
|
302
|
+
const instance = createInstance(stack);
|
|
303
|
+
const hours = createBusinessHours(stack, instance);
|
|
304
|
+
|
|
305
|
+
new Queue(stack, 'Queue', {
|
|
306
|
+
instance,
|
|
307
|
+
hoursOfOperation: hours,
|
|
308
|
+
name: 'test-queue',
|
|
309
|
+
maxContacts: 50,
|
|
310
|
+
status: QueueStatus.DISABLED,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Queue', {
|
|
314
|
+
HoursOfOperationArn: Match.anyValue(),
|
|
315
|
+
InstanceArn: Match.anyValue(),
|
|
316
|
+
MaxContacts: 50,
|
|
317
|
+
Name: 'test-queue',
|
|
318
|
+
Status: 'DISABLED',
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('passes outbound caller config through to CfnQueue', () => {
|
|
323
|
+
const stack = new Stack();
|
|
324
|
+
const instance = createInstance(stack);
|
|
325
|
+
const hours = createBusinessHours(stack, instance);
|
|
326
|
+
|
|
327
|
+
new Queue(stack, 'Queue', {
|
|
328
|
+
instance,
|
|
329
|
+
hoursOfOperation: hours,
|
|
330
|
+
name: 'Support',
|
|
331
|
+
outboundCallerConfig: {
|
|
332
|
+
outboundCallerIdName: 'Support',
|
|
333
|
+
outboundCallerIdNumberArn: 'arn:aws:connect:us-east-1:123456789012:phone-number/abc123',
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Queue', {
|
|
338
|
+
OutboundCallerConfig: {
|
|
339
|
+
OutboundCallerIdName: 'Support',
|
|
340
|
+
OutboundCallerIdNumberArn: 'arn:aws:connect:us-east-1:123456789012:phone-number/abc123',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('rejects an empty queue name', () => {
|
|
346
|
+
const stack = new Stack();
|
|
347
|
+
const instance = createInstance(stack);
|
|
348
|
+
const hours = createBusinessHours(stack, instance);
|
|
349
|
+
|
|
350
|
+
expect(() => new Queue(stack, 'Queue', {
|
|
351
|
+
instance,
|
|
352
|
+
hoursOfOperation: hours,
|
|
353
|
+
name: ' ',
|
|
354
|
+
})).toThrow(/name/);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('rejects negative maxContacts', () => {
|
|
358
|
+
const stack = new Stack();
|
|
359
|
+
const instance = createInstance(stack);
|
|
360
|
+
const hours = createBusinessHours(stack, instance);
|
|
361
|
+
|
|
362
|
+
expect(() => new Queue(stack, 'Queue', {
|
|
363
|
+
instance,
|
|
364
|
+
hoursOfOperation: hours,
|
|
365
|
+
name: 'Support',
|
|
366
|
+
maxContacts: -1,
|
|
367
|
+
})).toThrow(/maxContacts/);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('allows maxContacts of zero', () => {
|
|
371
|
+
const stack = new Stack();
|
|
372
|
+
const instance = createInstance(stack);
|
|
373
|
+
const hours = createBusinessHours(stack, instance);
|
|
374
|
+
|
|
375
|
+
new Queue(stack, 'Queue', {
|
|
376
|
+
instance,
|
|
377
|
+
hoursOfOperation: hours,
|
|
378
|
+
name: 'Support',
|
|
379
|
+
maxContacts: 0,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::Queue', {
|
|
383
|
+
MaxContacts: 0,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('imports existing queue by ARN', () => {
|
|
388
|
+
const stack = new Stack();
|
|
389
|
+
|
|
390
|
+
const queue = Queue.fromQueueArn(
|
|
391
|
+
stack,
|
|
392
|
+
'ImportedQueue',
|
|
393
|
+
'arn:aws:connect:us-east-1:123456789012:instance/abc123/queue/queue123',
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(queue.queueArn).toBe(
|
|
397
|
+
'arn:aws:connect:us-east-1:123456789012:instance/abc123/queue/queue123',
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('RoutingProfile', () => {
|
|
403
|
+
test('creates CfnRoutingProfile with queue associations and media concurrency', () => {
|
|
404
|
+
const stack = new Stack();
|
|
405
|
+
const instance = createInstance(stack);
|
|
406
|
+
const hours = createBusinessHours(stack, instance);
|
|
407
|
+
const queue = createQueue(stack, instance, hours);
|
|
408
|
+
|
|
409
|
+
new RoutingProfile(stack, 'RoutingProfile', {
|
|
410
|
+
instance,
|
|
411
|
+
name: 'Tier 1',
|
|
412
|
+
description: 'Tier 1 support agents',
|
|
413
|
+
defaultOutboundQueue: queue,
|
|
414
|
+
mediaConcurrencies: [
|
|
415
|
+
{ channel: Channel.VOICE, concurrency: 1 },
|
|
416
|
+
{ channel: Channel.CHAT, concurrency: 2 },
|
|
417
|
+
],
|
|
418
|
+
queueConfigs: [
|
|
419
|
+
{ queue, channel: Channel.VOICE, priority: 1, delay: 0 },
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::RoutingProfile', {
|
|
424
|
+
DefaultOutboundQueueArn: Match.anyValue(),
|
|
425
|
+
Description: 'Tier 1 support agents',
|
|
426
|
+
MediaConcurrencies: [
|
|
427
|
+
{
|
|
428
|
+
Channel: 'VOICE',
|
|
429
|
+
Concurrency: 1,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
Channel: 'CHAT',
|
|
433
|
+
Concurrency: 2,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
Name: 'Tier 1',
|
|
437
|
+
QueueConfigs: [
|
|
438
|
+
{
|
|
439
|
+
Delay: 0,
|
|
440
|
+
Priority: 1,
|
|
441
|
+
QueueReference: {
|
|
442
|
+
Channel: 'VOICE',
|
|
443
|
+
QueueArn: Match.anyValue(),
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('defaults queue config channel to voice', () => {
|
|
451
|
+
const stack = new Stack();
|
|
452
|
+
const instance = createInstance(stack);
|
|
453
|
+
const hours = createBusinessHours(stack, instance);
|
|
454
|
+
const queue = createQueue(stack, instance, hours);
|
|
455
|
+
|
|
456
|
+
new RoutingProfile(stack, 'RoutingProfile', {
|
|
457
|
+
instance,
|
|
458
|
+
name: 'Tier 1',
|
|
459
|
+
description: 'Tier 1 support agents',
|
|
460
|
+
defaultOutboundQueue: queue,
|
|
461
|
+
queueConfigs: [
|
|
462
|
+
{ queue, priority: 2, delay: 5 },
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::RoutingProfile', {
|
|
467
|
+
QueueConfigs: [
|
|
468
|
+
{
|
|
469
|
+
Delay: 5,
|
|
470
|
+
Priority: 2,
|
|
471
|
+
QueueReference: {
|
|
472
|
+
Channel: 'VOICE',
|
|
473
|
+
QueueArn: Match.anyValue(),
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('rejects an empty queueConfigs array', () => {
|
|
481
|
+
const stack = new Stack();
|
|
482
|
+
const instance = createInstance(stack);
|
|
483
|
+
const hours = createBusinessHours(stack, instance);
|
|
484
|
+
const queue = createQueue(stack, instance, hours);
|
|
485
|
+
|
|
486
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
487
|
+
instance,
|
|
488
|
+
name: 'Tier 1',
|
|
489
|
+
description: 'Tier 1 support agents',
|
|
490
|
+
defaultOutboundQueue: queue,
|
|
491
|
+
queueConfigs: [],
|
|
492
|
+
})).toThrow(/queueConfigs/);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('uses default media concurrency and queue config', () => {
|
|
496
|
+
const stack = new Stack();
|
|
497
|
+
const instance = createInstance(stack);
|
|
498
|
+
const hours = createBusinessHours(stack, instance);
|
|
499
|
+
const queue = createQueue(stack, instance, hours);
|
|
500
|
+
|
|
501
|
+
new RoutingProfile(stack, 'RoutingProfile', {
|
|
502
|
+
instance,
|
|
503
|
+
name: 'Tier 1',
|
|
504
|
+
description: 'Tier 1 support agents',
|
|
505
|
+
defaultOutboundQueue: queue,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::RoutingProfile', {
|
|
509
|
+
MediaConcurrencies: [
|
|
510
|
+
{
|
|
511
|
+
Channel: 'VOICE',
|
|
512
|
+
Concurrency: 1,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
QueueConfigs: [
|
|
516
|
+
{
|
|
517
|
+
Delay: 0,
|
|
518
|
+
Priority: 1,
|
|
519
|
+
QueueReference: {
|
|
520
|
+
Channel: 'VOICE',
|
|
521
|
+
QueueArn: Match.anyValue(),
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('rejects an empty mediaConcurrencies array', () => {
|
|
529
|
+
const stack = new Stack();
|
|
530
|
+
const instance = createInstance(stack);
|
|
531
|
+
const hours = createBusinessHours(stack, instance);
|
|
532
|
+
const queue = createQueue(stack, instance, hours);
|
|
533
|
+
|
|
534
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
535
|
+
instance,
|
|
536
|
+
name: 'Tier 1',
|
|
537
|
+
description: 'Tier 1 support agents',
|
|
538
|
+
defaultOutboundQueue: queue,
|
|
539
|
+
mediaConcurrencies: [],
|
|
540
|
+
})).toThrow(/mediaConcurrencies/);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('rejects invalid media concurrency', () => {
|
|
544
|
+
const stack = new Stack();
|
|
545
|
+
const instance = createInstance(stack);
|
|
546
|
+
const hours = createBusinessHours(stack, instance);
|
|
547
|
+
const queue = createQueue(stack, instance, hours);
|
|
548
|
+
|
|
549
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
550
|
+
instance,
|
|
551
|
+
name: 'Tier 1',
|
|
552
|
+
description: 'Tier 1 support agents',
|
|
553
|
+
defaultOutboundQueue: queue,
|
|
554
|
+
mediaConcurrencies: [
|
|
555
|
+
{ channel: Channel.VOICE, concurrency: 0 },
|
|
556
|
+
],
|
|
557
|
+
})).toThrow(/media concurrency/);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('rejects non-integer media concurrency', () => {
|
|
561
|
+
const stack = new Stack();
|
|
562
|
+
const instance = createInstance(stack);
|
|
563
|
+
const hours = createBusinessHours(stack, instance);
|
|
564
|
+
const queue = createQueue(stack, instance, hours);
|
|
565
|
+
|
|
566
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
567
|
+
instance,
|
|
568
|
+
name: 'Tier 1',
|
|
569
|
+
description: 'Tier 1 support agents',
|
|
570
|
+
defaultOutboundQueue: queue,
|
|
571
|
+
mediaConcurrencies: [
|
|
572
|
+
{ channel: Channel.VOICE, concurrency: 1.5 },
|
|
573
|
+
],
|
|
574
|
+
})).toThrow(/media concurrency/);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('rejects an empty routing profile name', () => {
|
|
578
|
+
const stack = new Stack();
|
|
579
|
+
const instance = createInstance(stack);
|
|
580
|
+
const hours = createBusinessHours(stack, instance);
|
|
581
|
+
const queue = createQueue(stack, instance, hours);
|
|
582
|
+
|
|
583
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
584
|
+
instance,
|
|
585
|
+
name: ' ',
|
|
586
|
+
description: 'Tier 1 support agents',
|
|
587
|
+
defaultOutboundQueue: queue,
|
|
588
|
+
})).toThrow(/name/);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('rejects an empty routing profile description', () => {
|
|
592
|
+
const stack = new Stack();
|
|
593
|
+
const instance = createInstance(stack);
|
|
594
|
+
const hours = createBusinessHours(stack, instance);
|
|
595
|
+
const queue = createQueue(stack, instance, hours);
|
|
596
|
+
|
|
597
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
598
|
+
instance,
|
|
599
|
+
name: 'Tier 1',
|
|
600
|
+
description: ' ',
|
|
601
|
+
defaultOutboundQueue: queue,
|
|
602
|
+
})).toThrow(/description/);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('rejects invalid queue priority and delay', () => {
|
|
606
|
+
const stack = new Stack();
|
|
607
|
+
const instance = createInstance(stack);
|
|
608
|
+
const hours = createBusinessHours(stack, instance);
|
|
609
|
+
const queue = createQueue(stack, instance, hours);
|
|
610
|
+
|
|
611
|
+
expect(() => new RoutingProfile(stack, 'RoutingProfile', {
|
|
612
|
+
instance,
|
|
613
|
+
name: 'Tier 1',
|
|
614
|
+
description: 'Tier 1 support agents',
|
|
615
|
+
defaultOutboundQueue: queue,
|
|
616
|
+
queueConfigs: [
|
|
617
|
+
{ queue, priority: 0 },
|
|
618
|
+
],
|
|
619
|
+
})).toThrow(/queue priority/);
|
|
620
|
+
|
|
621
|
+
const delayStack = new Stack();
|
|
622
|
+
const delayInstance = createInstance(delayStack);
|
|
623
|
+
const delayHours = createBusinessHours(delayStack, delayInstance);
|
|
624
|
+
const delayQueue = createQueue(delayStack, delayInstance, delayHours);
|
|
625
|
+
|
|
626
|
+
expect(() => new RoutingProfile(delayStack, 'RoutingProfile', {
|
|
627
|
+
instance: delayInstance,
|
|
628
|
+
name: 'Tier 1',
|
|
629
|
+
description: 'Tier 1 support agents',
|
|
630
|
+
defaultOutboundQueue: delayQueue,
|
|
631
|
+
queueConfigs: [
|
|
632
|
+
{ queue: delayQueue, delay: -1 },
|
|
633
|
+
],
|
|
634
|
+
})).toThrow(/queue delay/);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe('ContactFlow', () => {
|
|
639
|
+
test('creates CfnContactFlow with serialized JSON content', () => {
|
|
640
|
+
const stack = new Stack();
|
|
641
|
+
const instance = createInstance(stack);
|
|
642
|
+
|
|
643
|
+
new ContactFlow(stack, 'Flow', {
|
|
644
|
+
instance,
|
|
645
|
+
name: 'Inbound support',
|
|
646
|
+
contactFlowType: ContactFlowType.CUSTOMER_QUEUE,
|
|
647
|
+
state: ContactFlowState.ARCHIVED,
|
|
648
|
+
content: ContactFlow.stringify({
|
|
649
|
+
Version: '2019-10-30',
|
|
650
|
+
StartAction: 'disconnect',
|
|
651
|
+
Actions: [
|
|
652
|
+
{
|
|
653
|
+
Identifier: 'disconnect',
|
|
654
|
+
Type: 'DisconnectParticipant',
|
|
655
|
+
Parameters: {},
|
|
656
|
+
Transitions: {},
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
}),
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::ContactFlow', {
|
|
663
|
+
Content: Match.stringLikeRegexp('DisconnectParticipant'),
|
|
664
|
+
InstanceArn: Match.anyValue(),
|
|
665
|
+
Name: 'Inbound support',
|
|
666
|
+
State: 'ARCHIVED',
|
|
667
|
+
Type: 'CUSTOMER_QUEUE',
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test('uses default flow type and active state', () => {
|
|
672
|
+
const stack = new Stack();
|
|
673
|
+
const instance = createInstance(stack);
|
|
674
|
+
|
|
675
|
+
new ContactFlow(stack, 'Flow', {
|
|
676
|
+
instance,
|
|
677
|
+
name: 'Inbound support',
|
|
678
|
+
content: '{}',
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
Template.fromStack(stack).hasResourceProperties('AWS::Connect::ContactFlow', {
|
|
682
|
+
Name: 'Inbound support',
|
|
683
|
+
State: 'ACTIVE',
|
|
684
|
+
Type: 'CONTACT_FLOW',
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('rejects an empty contact flow name', () => {
|
|
689
|
+
const stack = new Stack();
|
|
690
|
+
const instance = createInstance(stack);
|
|
691
|
+
|
|
692
|
+
expect(() => new ContactFlow(stack, 'Flow', {
|
|
693
|
+
instance,
|
|
694
|
+
name: ' ',
|
|
695
|
+
content: '{}',
|
|
696
|
+
})).toThrow(/name/);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test('rejects invalid contact flow JSON', () => {
|
|
700
|
+
const stack = new Stack();
|
|
701
|
+
const instance = createInstance(stack);
|
|
702
|
+
|
|
703
|
+
expect(() => new ContactFlow(stack, 'Flow', {
|
|
704
|
+
instance,
|
|
705
|
+
name: 'Broken',
|
|
706
|
+
content: '{',
|
|
707
|
+
})).toThrow(/valid JSON/);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
test('rejects empty contact flow content', () => {
|
|
711
|
+
const stack = new Stack();
|
|
712
|
+
const instance = createInstance(stack);
|
|
713
|
+
|
|
714
|
+
expect(() => new ContactFlow(stack, 'Flow', {
|
|
715
|
+
instance,
|
|
716
|
+
name: 'Broken',
|
|
717
|
+
content: ' ',
|
|
718
|
+
})).toThrow(/content/);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('stringifies non-Error values thrown during JSON validation', () => {
|
|
722
|
+
const stack = new Stack();
|
|
723
|
+
const instance = createInstance(stack);
|
|
724
|
+
const parseSpy = jest.spyOn(JSON, 'parse').mockImplementation(() => {
|
|
725
|
+
throw 'not-an-error';
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
expect(() => new ContactFlow(stack, 'Flow', {
|
|
730
|
+
instance,
|
|
731
|
+
name: 'Broken',
|
|
732
|
+
content: '{}',
|
|
733
|
+
})).toThrow(/content must be valid JSON: not-an-error/);
|
|
734
|
+
} finally {
|
|
735
|
+
parseSpy.mockRestore();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
describe('LambdaIntegration', () => {
|
|
741
|
+
test('creates Connect integration association and Lambda invoke permission', () => {
|
|
742
|
+
const app = new App();
|
|
743
|
+
const stack = new Stack(app, 'Stack', {
|
|
744
|
+
env: {
|
|
745
|
+
account: '123456789012',
|
|
746
|
+
region: 'us-east-1',
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
const instance = createInstance(stack);
|
|
750
|
+
const handler = new lambda.Function(stack, 'Handler', {
|
|
751
|
+
code: lambda.Code.fromInline('exports.handler = async () => ({ ok: true });'),
|
|
752
|
+
handler: 'index.handler',
|
|
753
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
new LambdaIntegration(stack, 'Integration', {
|
|
757
|
+
instance,
|
|
758
|
+
function: handler,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const template = Template.fromStack(stack);
|
|
762
|
+
template.hasResourceProperties('AWS::Connect::IntegrationAssociation', {
|
|
763
|
+
InstanceId: Match.anyValue(),
|
|
764
|
+
IntegrationArn: Match.anyValue(),
|
|
765
|
+
IntegrationType: 'LAMBDA_FUNCTION',
|
|
766
|
+
});
|
|
767
|
+
template.hasResourceProperties('AWS::Lambda::Permission', {
|
|
768
|
+
Action: 'lambda:InvokeFunction',
|
|
769
|
+
Principal: 'connect.amazonaws.com',
|
|
770
|
+
SourceArn: Match.anyValue(),
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test('rejects a missing Lambda function', () => {
|
|
775
|
+
const stack = new Stack();
|
|
776
|
+
const instance = createInstance(stack);
|
|
777
|
+
|
|
778
|
+
expect(() => new LambdaIntegration(stack, 'Integration', {
|
|
779
|
+
instance,
|
|
780
|
+
function: undefined as unknown as lambda.IFunction,
|
|
781
|
+
})).toThrow(/function is required/);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('rejects a missing Connect instance', () => {
|
|
785
|
+
const stack = new Stack();
|
|
786
|
+
const handler = new lambda.Function(stack, 'Handler', {
|
|
787
|
+
code: lambda.Code.fromInline('exports.handler = async () => ({ ok: true });'),
|
|
788
|
+
handler: 'index.handler',
|
|
789
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
expect(() => new LambdaIntegration(stack, 'Integration', {
|
|
793
|
+
instance: undefined as unknown as ConnectInstance,
|
|
794
|
+
function: handler,
|
|
795
|
+
})).toThrow(/instance is required/);
|
|
796
|
+
});
|
|
797
|
+
});
|