@slates/test 1.0.0-rc.27 → 1.0.0-rc.29
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/dist/index.cjs +42 -7
- package/dist/index.d.cts +226 -6
- package/dist/index.d.ts +226 -6
- package/dist/index.module.js +37 -7
- package/package.json +4 -4
- package/src/optionList.test.ts +194 -0
- package/src/runtime.ts +70 -14
- package/src/triggerLifecycle.test.ts +618 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Slate,
|
|
3
|
+
SlateAuth,
|
|
4
|
+
SlateConfig,
|
|
5
|
+
SlateSpecification,
|
|
6
|
+
SlateTrigger,
|
|
7
|
+
triggerGroup
|
|
8
|
+
} from '@slates/provider';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import {
|
|
12
|
+
authenticateSlateTriggerGroupWebhookDelivery,
|
|
13
|
+
createLocalSlateTestClient,
|
|
14
|
+
expandSlateTriggerGroupWebhook,
|
|
15
|
+
listSlateTriggerGroupSelectionOptions,
|
|
16
|
+
pollSlateTriggerGroupEvents,
|
|
17
|
+
processSlateTriggerGroupWebhook,
|
|
18
|
+
registerSlateTriggerGroupWebhook,
|
|
19
|
+
renewSlateTriggerGroupWebhook
|
|
20
|
+
} from './index';
|
|
21
|
+
|
|
22
|
+
let fixture = (
|
|
23
|
+
options: {
|
|
24
|
+
selection?: 'required' | 'default';
|
|
25
|
+
legacy?: boolean;
|
|
26
|
+
expiresAt?: string | null;
|
|
27
|
+
renewAfter?: string | null;
|
|
28
|
+
invalidExpansion?: boolean;
|
|
29
|
+
reconcileIntervalSeconds?: number;
|
|
30
|
+
deliveryConfig?: boolean;
|
|
31
|
+
} = {}
|
|
32
|
+
) => {
|
|
33
|
+
let contexts: { operation: string; auth: unknown; config: unknown; input: any }[] = [];
|
|
34
|
+
let auth = SlateAuth.create<{ token: string }>()
|
|
35
|
+
.output(z.object({ token: z.string() }))
|
|
36
|
+
.addTokenAuth({
|
|
37
|
+
type: 'auth.token',
|
|
38
|
+
key: 'token',
|
|
39
|
+
name: 'Token',
|
|
40
|
+
inputSchema: z.object({ token: z.string() }),
|
|
41
|
+
getOutput: async ctx => ({ output: ctx.input })
|
|
42
|
+
});
|
|
43
|
+
let spec = SlateSpecification.create({
|
|
44
|
+
key: 'lifecycle',
|
|
45
|
+
name: 'Lifecycle',
|
|
46
|
+
description: 'Trigger lifecycle contract',
|
|
47
|
+
auth,
|
|
48
|
+
config: SlateConfig.create(z.object({ tenant: z.string() }))
|
|
49
|
+
});
|
|
50
|
+
let selectionSchema =
|
|
51
|
+
options.selection === 'required'
|
|
52
|
+
? z.object({ resource: z.string() })
|
|
53
|
+
: options.selection === 'default'
|
|
54
|
+
? z.object({ resource: z.string().default('default-resource') })
|
|
55
|
+
: undefined;
|
|
56
|
+
let record = (operation: string, ctx: { auth: unknown; config: unknown; input: any }) => {
|
|
57
|
+
contexts.push({ operation, auth: ctx.auth, config: ctx.config, input: ctx.input });
|
|
58
|
+
};
|
|
59
|
+
let registration = {
|
|
60
|
+
webhookRegistrationIdentifier: 'registration',
|
|
61
|
+
webhookRegistrationPayload: { secret: 'verification' },
|
|
62
|
+
...(options.expiresAt !== undefined ? { expiresAt: options.expiresAt } : {}),
|
|
63
|
+
...(options.renewAfter !== undefined ? { renewAfter: options.renewAfter } : {})
|
|
64
|
+
};
|
|
65
|
+
let group = triggerGroup(spec, { key: 'webhook', name: 'Webhook', selectionSchema })
|
|
66
|
+
.webhook({
|
|
67
|
+
reconcileIntervalSeconds: options.reconcileIntervalSeconds,
|
|
68
|
+
deliveryConfig: options.deliveryConfig
|
|
69
|
+
? {
|
|
70
|
+
userConfigSchema: z.object({ topicName: z.string() }),
|
|
71
|
+
fullConfigSchema: z.object({ topicName: z.string(), audience: z.string() }),
|
|
72
|
+
setup: async ctx => {
|
|
73
|
+
record('setup', ctx);
|
|
74
|
+
return {
|
|
75
|
+
webhookSetupDocument: `Deliver to ${ctx.input.webhookUrl}`,
|
|
76
|
+
partialWebhookRegistrationPayload: {}
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
finish: async ctx => {
|
|
80
|
+
record('finish', ctx);
|
|
81
|
+
return {
|
|
82
|
+
webhookRegistrationPayload: {
|
|
83
|
+
...ctx.input.userWebhookRegistrationPayload,
|
|
84
|
+
audience: ctx.input.webhookUrl
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
authenticate: async ctx => {
|
|
89
|
+
record('authenticate', ctx);
|
|
90
|
+
let status = Number(ctx.input.request.headers.get('x-reject'));
|
|
91
|
+
if (status) return { rejected: { status } };
|
|
92
|
+
let body = JSON.parse(await ctx.input.request.text()) as { keys: string[] };
|
|
93
|
+
return { routingKeys: body.keys };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
: undefined,
|
|
97
|
+
autoRegistration: {
|
|
98
|
+
webhookTargetList: async ctx => {
|
|
99
|
+
record('targets', ctx);
|
|
100
|
+
return { targets: [], nextPageToken: null };
|
|
101
|
+
},
|
|
102
|
+
webhookRegister: async ctx => {
|
|
103
|
+
record('register', ctx);
|
|
104
|
+
return registration;
|
|
105
|
+
},
|
|
106
|
+
webhookRenew: options.legacy
|
|
107
|
+
? undefined
|
|
108
|
+
: async ctx => {
|
|
109
|
+
record('renew', ctx);
|
|
110
|
+
return {
|
|
111
|
+
...registration,
|
|
112
|
+
webhookRegistrationIdentifier: 'renewed-registration'
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
webhookUnregister: async () => {}
|
|
116
|
+
},
|
|
117
|
+
process: async ctx => {
|
|
118
|
+
record('process', ctx);
|
|
119
|
+
return {
|
|
120
|
+
events: [
|
|
121
|
+
{
|
|
122
|
+
payload: { raw: true },
|
|
123
|
+
matchers: [{ tenant: 'tenant' }],
|
|
124
|
+
idempotencyKey: 'raw-key'
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
expandWebhook: options.legacy
|
|
130
|
+
? undefined
|
|
131
|
+
: async ctx => {
|
|
132
|
+
record('expand', ctx);
|
|
133
|
+
// Malformed output deliberately exercises the runtime contract boundary.
|
|
134
|
+
if (options.invalidExpansion) return { events: [{ payload: [] }] };
|
|
135
|
+
let payload = z
|
|
136
|
+
.object({ empty: z.boolean().optional(), preserve: z.boolean().optional() })
|
|
137
|
+
.nullable()
|
|
138
|
+
.parse(ctx.input.payload);
|
|
139
|
+
if (payload?.empty) return { events: [], updatedState: null };
|
|
140
|
+
if (payload?.preserve) return { events: [] };
|
|
141
|
+
return {
|
|
142
|
+
events: [
|
|
143
|
+
{ payload: { kind: 'selected', id: 'one' }, idempotencyKey: 'one' },
|
|
144
|
+
{ payload: { kind: 'other', id: 'two' }, idempotencyKey: 'two' },
|
|
145
|
+
{ payload: { kind: 'selected', id: 'three' }, idempotencyKey: 'three' }
|
|
146
|
+
],
|
|
147
|
+
updatedState: { cursor: 'next' }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.routingMatchers(async () => [{ tenant: 'tenant' }])
|
|
152
|
+
.build();
|
|
153
|
+
let pollBuilder = triggerGroup(spec, { key: 'poll', name: 'Poll', selectionSchema })
|
|
154
|
+
.polling({
|
|
155
|
+
pollEvents: async ctx => {
|
|
156
|
+
record('poll', ctx);
|
|
157
|
+
return { events: [], updatedState: ctx.input.state };
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.routingMatchers(async () => []);
|
|
161
|
+
if (selectionSchema) {
|
|
162
|
+
pollBuilder.selectionOptions({
|
|
163
|
+
resource: {
|
|
164
|
+
list: async ctx => {
|
|
165
|
+
record('options', ctx);
|
|
166
|
+
return {
|
|
167
|
+
options: [{ value: 'a', label: 'Resource A' }],
|
|
168
|
+
nextPageToken: ctx.input.pageToken ? null : 'next'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
let poll = pollBuilder.build();
|
|
175
|
+
let matches = 0;
|
|
176
|
+
let selectedInput = z.object({
|
|
177
|
+
kind: z.string().optional(),
|
|
178
|
+
id: z.string().optional(),
|
|
179
|
+
raw: z.boolean().optional()
|
|
180
|
+
});
|
|
181
|
+
let selected = SlateTrigger.create(spec, { key: 'selected', name: 'Selected' })
|
|
182
|
+
.triggerGroup(group)
|
|
183
|
+
.input(selectedInput)
|
|
184
|
+
.output(z.object({ id: z.string() }))
|
|
185
|
+
.matches(payload => {
|
|
186
|
+
matches++;
|
|
187
|
+
let event = selectedInput.safeParse(payload);
|
|
188
|
+
return event.success && (event.data.kind === 'selected' || event.data.raw === true);
|
|
189
|
+
})
|
|
190
|
+
.map(async ctx => {
|
|
191
|
+
let id = ctx.input.id ?? 'raw';
|
|
192
|
+
return { type: 'selected', id, output: { id } };
|
|
193
|
+
})
|
|
194
|
+
.build();
|
|
195
|
+
let slate = Slate.create({
|
|
196
|
+
spec,
|
|
197
|
+
tools: [],
|
|
198
|
+
triggers: [selected],
|
|
199
|
+
triggerGroups: [group, poll]
|
|
200
|
+
});
|
|
201
|
+
let client = createLocalSlateTestClient({
|
|
202
|
+
slate,
|
|
203
|
+
state: {
|
|
204
|
+
config: { tenant: 'tenant' },
|
|
205
|
+
auth: { authenticationMethodId: 'token', output: { token: 'credential' } }
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return { client, contexts, matches: () => matches };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
let registrationInput = {
|
|
212
|
+
triggerGroupId: 'webhook',
|
|
213
|
+
webhookTargetIdentifier: 'target',
|
|
214
|
+
webhookTargetPayload: { resource: 'target' },
|
|
215
|
+
webhookUrl: 'https://example.com/webhook'
|
|
216
|
+
};
|
|
217
|
+
let renewalInput = {
|
|
218
|
+
...registrationInput,
|
|
219
|
+
webhookRegistrationIdentifier: 'registration',
|
|
220
|
+
webhookRegistrationPayload: { secret: 'verification' },
|
|
221
|
+
operationId: 'stable-operation'
|
|
222
|
+
};
|
|
223
|
+
let expansionInput = {
|
|
224
|
+
triggerGroupId: 'webhook',
|
|
225
|
+
payload: { raw: true },
|
|
226
|
+
state: { cursor: 'before' },
|
|
227
|
+
deliveryId: 'delivery',
|
|
228
|
+
webhookRegistrationPayload: { secret: 'verification' }
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
describe('trigger lifecycle protocol helpers', () => {
|
|
232
|
+
it('passes the saved delivery config unchanged and independently from user selection', async () => {
|
|
233
|
+
let { client, contexts } = fixture({ deliveryConfig: true });
|
|
234
|
+
let deliveryConfig = {
|
|
235
|
+
topicName: 'projects/p/topics/t',
|
|
236
|
+
audience: 'https://example.com/delivery',
|
|
237
|
+
nested: { kept: ['as', 'saved'] }
|
|
238
|
+
};
|
|
239
|
+
await registerSlateTriggerGroupWebhook({
|
|
240
|
+
client,
|
|
241
|
+
...registrationInput,
|
|
242
|
+
operationId: 'initial-registration-operation',
|
|
243
|
+
deliveryConfig
|
|
244
|
+
});
|
|
245
|
+
await renewSlateTriggerGroupWebhook({ client, ...renewalInput, deliveryConfig });
|
|
246
|
+
expect(contexts.map(ctx => [ctx.operation, ctx.input.deliveryConfig])).toEqual([
|
|
247
|
+
['register', deliveryConfig],
|
|
248
|
+
['renew', deliveryConfig]
|
|
249
|
+
]);
|
|
250
|
+
expect(contexts[0]?.input.operationId).toBe('initial-registration-operation');
|
|
251
|
+
await registerSlateTriggerGroupWebhook({
|
|
252
|
+
client,
|
|
253
|
+
...registrationInput,
|
|
254
|
+
selection: { deliveryConfig }
|
|
255
|
+
});
|
|
256
|
+
expect(contexts.at(-1)?.input.deliveryConfig).toBeUndefined();
|
|
257
|
+
expect(contexts.at(-1)?.input.operationId).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('exposes the delivery config of an automatic group and completes it without credentials', async () => {
|
|
261
|
+
let { client, contexts } = fixture({ deliveryConfig: true });
|
|
262
|
+
expect((await client.getTriggerGroup('webhook')).triggerGroup.invocation).toMatchObject({
|
|
263
|
+
registration: {
|
|
264
|
+
mode: 'auto',
|
|
265
|
+
deliveryConfig: {
|
|
266
|
+
userConfigSchema: { type: 'object', required: ['topicName'] },
|
|
267
|
+
fullConfigSchema: { type: 'object', required: ['topicName', 'audience'] }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
client.clearAuth();
|
|
272
|
+
let webhookUrl = 'https://example.com/delivery';
|
|
273
|
+
let setup = await client.setupTriggerGroupWebhookManually({
|
|
274
|
+
triggerGroupId: 'webhook',
|
|
275
|
+
webhookUrl
|
|
276
|
+
});
|
|
277
|
+
expect(setup).toEqual({
|
|
278
|
+
webhookSetupDocument: `Deliver to ${webhookUrl}`,
|
|
279
|
+
partialWebhookRegistrationPayload: {}
|
|
280
|
+
});
|
|
281
|
+
let finished = await client.finishTriggerGroupWebhookManualSetup({
|
|
282
|
+
triggerGroupId: 'webhook',
|
|
283
|
+
webhookUrl,
|
|
284
|
+
partialWebhookRegistrationPayload: setup.partialWebhookRegistrationPayload,
|
|
285
|
+
userWebhookRegistrationPayload: { topicName: 'projects/p/topics/t' }
|
|
286
|
+
});
|
|
287
|
+
expect(finished.webhookRegistrationPayload).toEqual({
|
|
288
|
+
topicName: 'projects/p/topics/t',
|
|
289
|
+
audience: webhookUrl
|
|
290
|
+
});
|
|
291
|
+
expect(contexts.map(ctx => [ctx.operation, ctx.auth, ctx.config])).toEqual([
|
|
292
|
+
['setup', {}, {}],
|
|
293
|
+
['finish', {}, {}]
|
|
294
|
+
]);
|
|
295
|
+
await expect(
|
|
296
|
+
fixture().client.setupTriggerGroupWebhookManually({
|
|
297
|
+
triggerGroupId: 'webhook',
|
|
298
|
+
webhookUrl
|
|
299
|
+
})
|
|
300
|
+
).rejects.toThrow('does not support manual webhook registration');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('authenticates a delivery through the delivery config without credentials', async () => {
|
|
304
|
+
let { client, contexts } = fixture({ deliveryConfig: true });
|
|
305
|
+
client.clearAuth();
|
|
306
|
+
let deliveryConfig = {
|
|
307
|
+
topicName: 'projects/p/topics/t',
|
|
308
|
+
audience: 'https://example.com/d'
|
|
309
|
+
};
|
|
310
|
+
let delivery = {
|
|
311
|
+
client,
|
|
312
|
+
triggerGroupId: 'webhook',
|
|
313
|
+
url: 'https://example.com/d',
|
|
314
|
+
deliveryConfig
|
|
315
|
+
};
|
|
316
|
+
expect(
|
|
317
|
+
await authenticateSlateTriggerGroupWebhookDelivery({
|
|
318
|
+
...delivery,
|
|
319
|
+
body: JSON.stringify({ keys: ['resource:one', 'resource:two'] })
|
|
320
|
+
})
|
|
321
|
+
).toEqual({ routingKeys: ['resource:one', 'resource:two'], rejected: null });
|
|
322
|
+
expect(
|
|
323
|
+
await authenticateSlateTriggerGroupWebhookDelivery({
|
|
324
|
+
...delivery,
|
|
325
|
+
body: JSON.stringify({ keys: [] })
|
|
326
|
+
})
|
|
327
|
+
).toEqual({ routingKeys: [], rejected: null });
|
|
328
|
+
expect(
|
|
329
|
+
await authenticateSlateTriggerGroupWebhookDelivery({
|
|
330
|
+
...delivery,
|
|
331
|
+
headers: { 'x-reject': '503' }
|
|
332
|
+
})
|
|
333
|
+
).toEqual({ routingKeys: [], rejected: { status: 503 } });
|
|
334
|
+
expect(contexts.map(ctx => [ctx.operation, ctx.auth, ctx.config])).toEqual(
|
|
335
|
+
Array(3).fill(['authenticate', {}, {}])
|
|
336
|
+
);
|
|
337
|
+
expect(contexts.map(ctx => ctx.input.deliveryConfig)).toEqual(
|
|
338
|
+
Array(3).fill(deliveryConfig)
|
|
339
|
+
);
|
|
340
|
+
await expect(
|
|
341
|
+
authenticateSlateTriggerGroupWebhookDelivery({
|
|
342
|
+
...delivery,
|
|
343
|
+
client: fixture().client,
|
|
344
|
+
body: JSON.stringify({ keys: [] })
|
|
345
|
+
})
|
|
346
|
+
).rejects.toThrow('does not support webhook delivery authentication');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('advertises schemas and capabilities while preserving old group specifications', async () => {
|
|
350
|
+
let { client } = fixture({ selection: 'default' });
|
|
351
|
+
expect((await client.getTriggerGroup('webhook')).triggerGroup).toMatchObject({
|
|
352
|
+
selectionSchema: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: { resource: { type: 'string', default: 'default-resource' } }
|
|
355
|
+
},
|
|
356
|
+
invocation: { type: 'webhook', supportsExpansion: true, supportsRenewal: true }
|
|
357
|
+
});
|
|
358
|
+
let legacy = fixture({ legacy: true });
|
|
359
|
+
let oldGroup = (await legacy.client.getTriggerGroup('webhook')).triggerGroup;
|
|
360
|
+
expect(oldGroup).not.toHaveProperty('selectionSchema');
|
|
361
|
+
expect(oldGroup.invocation).toEqual({ type: 'webhook', registration: { mode: 'auto' } });
|
|
362
|
+
expect(
|
|
363
|
+
(await client.getTriggerGroup('webhook')).triggerGroup.selectionSchema
|
|
364
|
+
).not.toHaveProperty('required');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('declares bounded reconciliation and passes scheduled null payloads unchanged', async () => {
|
|
368
|
+
for (let reconcileIntervalSeconds of [
|
|
369
|
+
0,
|
|
370
|
+
899,
|
|
371
|
+
900.5,
|
|
372
|
+
Number.NaN,
|
|
373
|
+
Number.POSITIVE_INFINITY
|
|
374
|
+
]) {
|
|
375
|
+
expect(() => fixture({ reconcileIntervalSeconds })).toThrow(
|
|
376
|
+
'integer of at least 900 seconds'
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
expect(() => fixture({ legacy: true, reconcileIntervalSeconds: 900 })).toThrow(
|
|
380
|
+
'requires an expandWebhook handler'
|
|
381
|
+
);
|
|
382
|
+
let { client, contexts } = fixture({ reconcileIntervalSeconds: 900 });
|
|
383
|
+
expect((await client.getTriggerGroup('webhook')).triggerGroup.invocation).toMatchObject({
|
|
384
|
+
reconcileIntervalSeconds: 900,
|
|
385
|
+
supportsExpansion: true
|
|
386
|
+
});
|
|
387
|
+
let result = await expandSlateTriggerGroupWebhook({
|
|
388
|
+
client,
|
|
389
|
+
...expansionInput,
|
|
390
|
+
payload: null,
|
|
391
|
+
deliveryId: 'scheduled-run'
|
|
392
|
+
});
|
|
393
|
+
expect(result.events).toHaveLength(3);
|
|
394
|
+
expect(contexts[0]?.input).toMatchObject({
|
|
395
|
+
payload: null,
|
|
396
|
+
deliveryId: 'scheduled-run',
|
|
397
|
+
state: expansionInput.state,
|
|
398
|
+
webhookRegistrationPayload: expansionInput.webhookRegistrationPayload
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('verifies without credentials and expands zero or many events with connection context', async () => {
|
|
403
|
+
let { client, contexts, matches } = fixture({ selection: 'required' });
|
|
404
|
+
let processed = await processSlateTriggerGroupWebhook({
|
|
405
|
+
client,
|
|
406
|
+
triggerGroupId: 'webhook',
|
|
407
|
+
url: registrationInput.webhookUrl,
|
|
408
|
+
webhookRegistrationPayload: expansionInput.webhookRegistrationPayload
|
|
409
|
+
});
|
|
410
|
+
expect(processed).toMatchObject({
|
|
411
|
+
requiresExpansion: true,
|
|
412
|
+
events: [{ triggerIds: [], idempotencyKey: 'raw-key' }]
|
|
413
|
+
});
|
|
414
|
+
expect(matches()).toBe(0);
|
|
415
|
+
expect(contexts[0]).toMatchObject({ operation: 'process', auth: {}, config: {} });
|
|
416
|
+
let expanded = await expandSlateTriggerGroupWebhook({
|
|
417
|
+
client,
|
|
418
|
+
...expansionInput,
|
|
419
|
+
selection: { resource: 'chosen' }
|
|
420
|
+
});
|
|
421
|
+
expect(expanded).toMatchObject({
|
|
422
|
+
updatedState: { cursor: 'next' },
|
|
423
|
+
events: [
|
|
424
|
+
{ idempotencyKey: 'one', triggerIds: ['selected'] },
|
|
425
|
+
{ idempotencyKey: 'two', triggerIds: [] },
|
|
426
|
+
{ idempotencyKey: 'three', triggerIds: ['selected'] }
|
|
427
|
+
]
|
|
428
|
+
});
|
|
429
|
+
expect(contexts[1]).toMatchObject({
|
|
430
|
+
operation: 'expand',
|
|
431
|
+
auth: { token: 'credential' },
|
|
432
|
+
config: { tenant: 'tenant' },
|
|
433
|
+
input: {
|
|
434
|
+
payload: expansionInput.payload,
|
|
435
|
+
state: expansionInput.state,
|
|
436
|
+
deliveryId: expansionInput.deliveryId,
|
|
437
|
+
webhookRegistrationPayload: expansionInput.webhookRegistrationPayload,
|
|
438
|
+
selection: { resource: 'chosen' }
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
expect(matches()).toBe(3);
|
|
442
|
+
expect(
|
|
443
|
+
await client.mapTriggerEvent('selected', expanded.events[0]!.payload)
|
|
444
|
+
).toMatchObject({ output: { id: 'one' } });
|
|
445
|
+
let cleared = await expandSlateTriggerGroupWebhook({
|
|
446
|
+
client,
|
|
447
|
+
...expansionInput,
|
|
448
|
+
payload: { empty: true },
|
|
449
|
+
selection: { resource: 'chosen' }
|
|
450
|
+
});
|
|
451
|
+
expect(cleared).toMatchObject({ events: [], updatedState: null });
|
|
452
|
+
let preserved = await expandSlateTriggerGroupWebhook({
|
|
453
|
+
client,
|
|
454
|
+
...expansionInput,
|
|
455
|
+
payload: { preserve: true },
|
|
456
|
+
selection: { resource: 'chosen' }
|
|
457
|
+
});
|
|
458
|
+
expect(preserved.events).toEqual([]);
|
|
459
|
+
expect(preserved).not.toHaveProperty('updatedState');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('validates selection before every affected handler and applies accepted defaults', async () => {
|
|
463
|
+
let { client, contexts } = fixture({ selection: 'required' });
|
|
464
|
+
let calls = [
|
|
465
|
+
() => client.listTriggerGroupWebhookTargets({ triggerGroupId: 'webhook' }),
|
|
466
|
+
() => registerSlateTriggerGroupWebhook({ client, ...registrationInput }),
|
|
467
|
+
() => renewSlateTriggerGroupWebhook({ client, ...renewalInput }),
|
|
468
|
+
() => expandSlateTriggerGroupWebhook({ client, ...expansionInput }),
|
|
469
|
+
() => pollSlateTriggerGroupEvents({ client, triggerGroupId: 'poll' })
|
|
470
|
+
];
|
|
471
|
+
for (let call of calls) await expect(call()).rejects.toThrow('Invalid selection');
|
|
472
|
+
expect(contexts).toEqual([]);
|
|
473
|
+
let defaults = fixture({ selection: 'default' });
|
|
474
|
+
await defaults.client.listTriggerGroupWebhookTargets({ triggerGroupId: 'webhook' });
|
|
475
|
+
await registerSlateTriggerGroupWebhook({ client: defaults.client, ...registrationInput });
|
|
476
|
+
await renewSlateTriggerGroupWebhook({ client: defaults.client, ...renewalInput });
|
|
477
|
+
await expandSlateTriggerGroupWebhook({ client: defaults.client, ...expansionInput });
|
|
478
|
+
await pollSlateTriggerGroupEvents({ client: defaults.client, triggerGroupId: 'poll' });
|
|
479
|
+
expect(defaults.contexts.map(ctx => ctx.input.selection)).toEqual(
|
|
480
|
+
Array(5).fill({ resource: 'default-resource' })
|
|
481
|
+
);
|
|
482
|
+
await pollSlateTriggerGroupEvents({
|
|
483
|
+
client: defaults.client,
|
|
484
|
+
triggerGroupId: 'poll',
|
|
485
|
+
selection: { resource: 'explicit' }
|
|
486
|
+
});
|
|
487
|
+
expect(defaults.contexts.at(-1)?.input.selection).toEqual({ resource: 'explicit' });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('lists options for a marked selection field from an incomplete selection', async () => {
|
|
491
|
+
let { client, contexts } = fixture({ selection: 'required' });
|
|
492
|
+
let marked = (await client.getTriggerGroup('poll')).triggerGroup.selectionSchema;
|
|
493
|
+
let unmarked = (await client.getTriggerGroup('webhook')).triggerGroup.selectionSchema;
|
|
494
|
+
expect(marked?.properties.resource['x-slates-options']).toBe(true);
|
|
495
|
+
expect(unmarked?.properties.resource).not.toHaveProperty('x-slates-options');
|
|
496
|
+
|
|
497
|
+
let first = await listSlateTriggerGroupSelectionOptions({
|
|
498
|
+
client,
|
|
499
|
+
triggerGroupId: 'poll',
|
|
500
|
+
field: 'resource',
|
|
501
|
+
query: ' res ',
|
|
502
|
+
selection: { other: 'entered' }
|
|
503
|
+
});
|
|
504
|
+
expect(first).toMatchObject({
|
|
505
|
+
options: [{ value: 'a', label: 'Resource A' }],
|
|
506
|
+
nextPageToken: 'next'
|
|
507
|
+
});
|
|
508
|
+
expect(contexts.at(-1)).toMatchObject({
|
|
509
|
+
operation: 'options',
|
|
510
|
+
input: { query: 'res', pageToken: null, selection: { other: 'entered' } }
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
let last = await listSlateTriggerGroupSelectionOptions({
|
|
514
|
+
client,
|
|
515
|
+
triggerGroupId: 'poll',
|
|
516
|
+
field: 'resource',
|
|
517
|
+
pageToken: 'next'
|
|
518
|
+
});
|
|
519
|
+
expect(last.nextPageToken).toBeNull();
|
|
520
|
+
expect(contexts.at(-1)?.input).toMatchObject({ query: null, selection: {} });
|
|
521
|
+
|
|
522
|
+
for (let target of [
|
|
523
|
+
{ triggerGroupId: 'poll', field: 'missing' },
|
|
524
|
+
{ triggerGroupId: 'poll', field: 'constructor' },
|
|
525
|
+
{ triggerGroupId: 'webhook', field: 'resource' }
|
|
526
|
+
]) {
|
|
527
|
+
await expect(
|
|
528
|
+
listSlateTriggerGroupSelectionOptions({ client, ...target })
|
|
529
|
+
).rejects.toThrow('has no options');
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('keeps groups without selection, expansion, or renewal backward compatible', async () => {
|
|
534
|
+
let { client, contexts } = fixture({ legacy: true });
|
|
535
|
+
await registerSlateTriggerGroupWebhook({ client, ...registrationInput });
|
|
536
|
+
await pollSlateTriggerGroupEvents({ client, triggerGroupId: 'poll' });
|
|
537
|
+
expect(contexts.map(ctx => ctx.input.selection)).toEqual([{}, {}]);
|
|
538
|
+
let processed = await processSlateTriggerGroupWebhook({
|
|
539
|
+
client,
|
|
540
|
+
triggerGroupId: 'webhook',
|
|
541
|
+
url: registrationInput.webhookUrl
|
|
542
|
+
});
|
|
543
|
+
expect(processed.events[0]?.triggerIds).toEqual(['selected']);
|
|
544
|
+
expect(processed).not.toHaveProperty('requiresExpansion');
|
|
545
|
+
await expect(renewSlateTriggerGroupWebhook({ client, ...renewalInput })).rejects.toThrow(
|
|
546
|
+
'does not support webhook renewal'
|
|
547
|
+
);
|
|
548
|
+
await expect(
|
|
549
|
+
expandSlateTriggerGroupWebhook({ client, ...expansionInput })
|
|
550
|
+
).rejects.toThrow('does not support webhook expansion');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('requires authenticated context for expansion and renewal', async () => {
|
|
554
|
+
let { client, contexts } = fixture();
|
|
555
|
+
client.clearAuth();
|
|
556
|
+
await expect(
|
|
557
|
+
expandSlateTriggerGroupWebhook({ client, ...expansionInput })
|
|
558
|
+
).rejects.toMatchObject({
|
|
559
|
+
data: { code: 'request.precondition_failed' }
|
|
560
|
+
});
|
|
561
|
+
await expect(
|
|
562
|
+
renewSlateTriggerGroupWebhook({ client, ...renewalInput })
|
|
563
|
+
).rejects.toMatchObject({
|
|
564
|
+
data: { code: 'request.precondition_failed' }
|
|
565
|
+
});
|
|
566
|
+
expect(contexts).toEqual([]);
|
|
567
|
+
await processSlateTriggerGroupWebhook({
|
|
568
|
+
client,
|
|
569
|
+
triggerGroupId: 'webhook',
|
|
570
|
+
url: registrationInput.webhookUrl
|
|
571
|
+
});
|
|
572
|
+
expect(contexts[0]).toMatchObject({ operation: 'process', auth: {}, config: {} });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('passes stable renewal identity and validates nullable ISO scheduling metadata', async () => {
|
|
576
|
+
let { client, contexts } = fixture({
|
|
577
|
+
expiresAt: '2026-12-01T12:00:00+01:00',
|
|
578
|
+
renewAfter: null
|
|
579
|
+
});
|
|
580
|
+
let registered = await registerSlateTriggerGroupWebhook({ client, ...registrationInput });
|
|
581
|
+
expect(registered).toMatchObject({
|
|
582
|
+
expiresAt: '2026-12-01T12:00:00+01:00',
|
|
583
|
+
renewAfter: null
|
|
584
|
+
});
|
|
585
|
+
let renewed = await renewSlateTriggerGroupWebhook({
|
|
586
|
+
client,
|
|
587
|
+
...renewalInput,
|
|
588
|
+
selection: { resource: 'chosen' }
|
|
589
|
+
});
|
|
590
|
+
expect(renewed).toMatchObject({
|
|
591
|
+
webhookRegistrationIdentifier: 'renewed-registration',
|
|
592
|
+
expiresAt: registered.expiresAt,
|
|
593
|
+
renewAfter: null
|
|
594
|
+
});
|
|
595
|
+
expect(contexts.at(-1)).toMatchObject({
|
|
596
|
+
operation: 'renew',
|
|
597
|
+
auth: { token: 'credential' },
|
|
598
|
+
config: { tenant: 'tenant' },
|
|
599
|
+
input: {
|
|
600
|
+
operationId: 'stable-operation',
|
|
601
|
+
webhookRegistrationIdentifier: 'registration',
|
|
602
|
+
webhookTargetIdentifier: 'target',
|
|
603
|
+
selection: { resource: 'chosen' }
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
let invalid = fixture({ expiresAt: 'tomorrow' });
|
|
607
|
+
await expect(
|
|
608
|
+
registerSlateTriggerGroupWebhook({ client: invalid.client, ...registrationInput })
|
|
609
|
+
).rejects.toThrow('invalid result');
|
|
610
|
+
await expect(
|
|
611
|
+
renewSlateTriggerGroupWebhook({ client: invalid.client, ...renewalInput })
|
|
612
|
+
).rejects.toThrow('invalid result');
|
|
613
|
+
let invalidExpansion = fixture({ invalidExpansion: true });
|
|
614
|
+
await expect(
|
|
615
|
+
expandSlateTriggerGroupWebhook({ client: invalidExpansion.client, ...expansionInput })
|
|
616
|
+
).rejects.toThrow('invalid result');
|
|
617
|
+
});
|
|
618
|
+
});
|