@antasphere/chassis-contract 1.1.0 → 1.3.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.
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ webhookDeliveriesListQuerySchema,
4
+ webhookDeliveryParamsSchema,
5
+ webhookEndpointCreateSchema,
6
+ webhookEndpointCreatedSchema,
7
+ webhookEndpointSchema,
8
+ webhookEndpointUpdateSchema,
9
+ webhookEventPatternSchema,
10
+ webhookFiltersSchema,
11
+ webhookUrlSchema
12
+ } from '../src/index.js';
13
+ import {
14
+ webhookCatalogRoute,
15
+ webhookCreateRoute,
16
+ webhookDeliveryGetRoute,
17
+ webhookRedeliverRoute
18
+ } from '../src/routes/index.js';
19
+
20
+ const ok = (schema: { safeParse: (v: unknown) => { success: boolean } }, v: unknown) =>
21
+ schema.safeParse(v).success;
22
+
23
+ describe('webhook event patterns', () => {
24
+ it('takes an exact type, a family or everything', () => {
25
+ for (const v of ['thing.created', 'thing.*', '*', 'some_thing.status_changed', 'a1.b2']) {
26
+ expect(ok(webhookEventPatternSchema, v), v).toBe(true);
27
+ }
28
+ });
29
+
30
+ it('refuses anything else', () => {
31
+ for (const v of [
32
+ '',
33
+ 'thing',
34
+ '*.created',
35
+ 'thing.created.extra',
36
+ 'Thing.created',
37
+ '1thing.x',
38
+ 'thing.',
39
+ '**',
40
+ `a.${'b'.repeat(80)}`
41
+ ]) {
42
+ expect(ok(webhookEventPatternSchema, v), v).toBe(false);
43
+ }
44
+ });
45
+ });
46
+
47
+ describe('webhook filters', () => {
48
+ it('maps subject keys to non-empty id lists', () => {
49
+ expect(ok(webhookFiltersSchema, {})).toBe(true);
50
+ expect(ok(webhookFiltersSchema, { thing: ['a', 'b'], project: ['p'] })).toBe(true);
51
+ });
52
+
53
+ it('refuses a bad key, an empty list, an empty id, a control character and more than 10 keys', () => {
54
+ expect(ok(webhookFiltersSchema, { Thing: ['a'] })).toBe(false);
55
+ expect(ok(webhookFiltersSchema, { thing: [] })).toBe(false);
56
+ expect(ok(webhookFiltersSchema, { thing: [''] })).toBe(false);
57
+ expect(ok(webhookFiltersSchema, { thing: ['a\u0000'] })).toBe(false);
58
+ expect(ok(webhookFiltersSchema, { thing: Array.from({ length: 101 }, (_, i) => `id${i}`) })).toBe(false);
59
+ const eleven = Object.fromEntries(Array.from({ length: 11 }, (_, i) => [`k${i}`, ['x']]));
60
+ expect(ok(webhookFiltersSchema, eleven)).toBe(false);
61
+ const ten = Object.fromEntries(Array.from({ length: 10 }, (_, i) => [`k${i}`, ['x']]));
62
+ expect(ok(webhookFiltersSchema, ten)).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe('webhook URL', () => {
67
+ it('takes http and https URLs', () => {
68
+ expect(ok(webhookUrlSchema, 'https://example.com/hooks')).toBe(true);
69
+ expect(ok(webhookUrlSchema, 'http://example.com:8080/x?y=1')).toBe(true);
70
+ });
71
+
72
+ it('refuses another scheme, credentials, whitespace and a non-URL', () => {
73
+ for (const v of [
74
+ 'ftp://example.com',
75
+ 'javascript:alert(1)',
76
+ 'https://user:pass@example.com',
77
+ 'https://user@example.com',
78
+ 'https://example.com/a\nb',
79
+ ' https://example.com',
80
+ 'example.com',
81
+ `https://example.com/${'a'.repeat(2048)}`
82
+ ]) {
83
+ expect(ok(webhookUrlSchema, v), v).toBe(false);
84
+ }
85
+ });
86
+ });
87
+
88
+ describe('webhook endpoint create and update', () => {
89
+ it('fills the defaults on create', () => {
90
+ const parsed = webhookEndpointCreateSchema.parse({ url: 'https://example.com', events: ['thing.*'] });
91
+ expect(parsed).toEqual({
92
+ url: 'https://example.com',
93
+ description: '',
94
+ events: ['thing.*'],
95
+ filters: {},
96
+ enabled: true
97
+ });
98
+ });
99
+
100
+ it('needs at least one event, at most 50', () => {
101
+ expect(ok(webhookEndpointCreateSchema, { url: 'https://example.com', events: [] })).toBe(false);
102
+ expect(
103
+ ok(webhookEndpointCreateSchema, { url: 'https://example.com', events: Array(51).fill('thing.created') })
104
+ ).toBe(false);
105
+ });
106
+
107
+ it('defaults nothing on update and refuses an empty change', () => {
108
+ expect(webhookEndpointUpdateSchema.parse({ enabled: false })).toEqual({ enabled: false });
109
+ expect(ok(webhookEndpointUpdateSchema, {})).toBe(false);
110
+ });
111
+ });
112
+
113
+ describe('the signing secret', () => {
114
+ it('is never part of an endpoint, only of the created answer', () => {
115
+ expect('secret' in webhookEndpointSchema.shape).toBe(false);
116
+ expect('secret' in webhookEndpointCreatedSchema.shape).toBe(true);
117
+ });
118
+ });
119
+
120
+ describe('webhook params and queries', () => {
121
+ it('takes uuids only', () => {
122
+ const id = '00000000-0000-4000-8000-000000000000';
123
+ expect(ok(webhookDeliveryParamsSchema, { id, deliveryId: id })).toBe(true);
124
+ expect(ok(webhookDeliveryParamsSchema, { id, deliveryId: 'nope' })).toBe(false);
125
+ });
126
+
127
+ it('filters deliveries by a known status', () => {
128
+ expect(ok(webhookDeliveriesListQuerySchema, { status: 'held' })).toBe(true);
129
+ expect(ok(webhookDeliveriesListQuerySchema, { status: 'lost' })).toBe(false);
130
+ });
131
+ });
132
+
133
+ describe('webhook routes', () => {
134
+ it('declares the paths and the answers the server registers', () => {
135
+ expect(webhookCatalogRoute.path).toBe('/webhooks/events');
136
+ expect(Object.keys(webhookCreateRoute.responses)).toContain('201');
137
+ expect(webhookDeliveryGetRoute.path).toBe('/webhooks/{id}/deliveries/{deliveryId}');
138
+ expect(webhookRedeliverRoute.method).toBe('post');
139
+ expect(Object.keys(webhookRedeliverRoute.responses)).toContain('201');
140
+ });
141
+ });