@yopdev/dev-server 3.0.3-beta.6 → 3.0.3-beta.7
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/__tests__/cloudformation-event-proxy.test.ts +137 -0
- package/__tests__/event-proxy.template +35 -0
- package/dist/__tests__/cloudformation-event-proxy.test.d.ts +1 -0
- package/dist/__tests__/cloudformation-event-proxy.test.js +123 -0
- package/dist/src/cloudformation-event-proxy.js +3 -2
- package/package.json +1 -1
- package/src/cloudformation-event-proxy.ts +3 -10
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { snsEventsFromCloudFormationTemplate } from "../src/cloudformation-event-proxy";
|
|
3
|
+
import { Network } from "testcontainers"
|
|
4
|
+
|
|
5
|
+
const SUBJECT_IS_UNUSED = 'anything'
|
|
6
|
+
const emptyDevServerConfig = async () =>
|
|
7
|
+
new Network().start().then((network) => ({
|
|
8
|
+
raw: {
|
|
9
|
+
region: "",
|
|
10
|
+
endpoint: "",
|
|
11
|
+
credentials: {
|
|
12
|
+
accessKeyId: "",
|
|
13
|
+
secretAccessKey: ""
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
network: network,
|
|
17
|
+
sqs: undefined,
|
|
18
|
+
sns: undefined,
|
|
19
|
+
s3: undefined,
|
|
20
|
+
dynamo: undefined,
|
|
21
|
+
eventsProxy: {
|
|
22
|
+
topic: {
|
|
23
|
+
arn: ""
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
describe('the cloudformation event proxy', () => {
|
|
29
|
+
describe('the handler matchers', () => {
|
|
30
|
+
const promisedEvents = emptyDevServerConfig().then((config) => snsEventsFromCloudFormationTemplate('test-events', {
|
|
31
|
+
template: () => './__tests__/event-proxy.template',
|
|
32
|
+
handlers: () => '../dist/__tests__/stub-handlers.js',
|
|
33
|
+
handlerNameResolver: (n) => n,
|
|
34
|
+
}).start(config))
|
|
35
|
+
|
|
36
|
+
describe('unfiltered event', () => {
|
|
37
|
+
const UNFILTERED_FUNCTION_TEMPLATE_POSITION = 0
|
|
38
|
+
it('unfiltered event returns true', async () => {
|
|
39
|
+
const events = await promisedEvents;
|
|
40
|
+
return expect(events[UNFILTERED_FUNCTION_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('single attribute filter', () => {
|
|
45
|
+
const SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION = 1
|
|
46
|
+
it.each(['filter-1-value-1', 'filter-1-value-2'])('returns true when matched', async (validFilterValue) => {
|
|
47
|
+
const events = await promisedEvents;
|
|
48
|
+
return expect(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
49
|
+
Filter1: { Type: 'String', Value: validFilterValue }
|
|
50
|
+
})).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns false when missing', async () => {
|
|
54
|
+
const events = await promisedEvents;
|
|
55
|
+
return expect(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns false when not matched', async () => {
|
|
59
|
+
const events = await promisedEvents;
|
|
60
|
+
return expect(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
61
|
+
Unexpected: { Type: 'String', Value: 'filter-1-value-1' }
|
|
62
|
+
})).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns false when not matched', async () => {
|
|
66
|
+
const events = await promisedEvents;
|
|
67
|
+
return expect(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
68
|
+
Filter1: { Type: 'String', Value: 'unexpected-value' }
|
|
69
|
+
})).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('many attributes filter', () => {
|
|
74
|
+
const MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION = 2
|
|
75
|
+
it.each([
|
|
76
|
+
{ Filter1: 'filter-1-value-1', Filter2: 'filter-2-value-1' },
|
|
77
|
+
{ Filter1: 'filter-1-value-1', Filter2: 'filter-2-value-2' },
|
|
78
|
+
{ Filter1: 'filter-1-value-2', Filter2: 'filter-2-value-1' },
|
|
79
|
+
{ Filter1: 'filter-1-value-2', Filter2: 'filter-2-value-2' },
|
|
80
|
+
])('returns true when all matched', async (expectedFilters) => {
|
|
81
|
+
const events = await promisedEvents;
|
|
82
|
+
const filters = Object.entries(expectedFilters).reduce((acc, [key, value]) => {
|
|
83
|
+
acc[key] = {
|
|
84
|
+
Type: 'String',
|
|
85
|
+
Value: value
|
|
86
|
+
};
|
|
87
|
+
return acc;
|
|
88
|
+
}, {})
|
|
89
|
+
|
|
90
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, filters)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns false when both attributes missing', async () => {
|
|
94
|
+
const events = await promisedEvents;
|
|
95
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns false when first filter missing', async () => {
|
|
99
|
+
const events = await promisedEvents;
|
|
100
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
101
|
+
Filter2: { Type: 'String', Value: 'filter-2-value-1' }
|
|
102
|
+
})).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('returns false when second filter missing', async () => {
|
|
106
|
+
const events = await promisedEvents;
|
|
107
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
108
|
+
Filter1: { Type: 'String', Value: 'filter-2-value-1' }
|
|
109
|
+
})).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('returns false when first filter does not match', async () => {
|
|
113
|
+
const events = await promisedEvents;
|
|
114
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
115
|
+
Filter1: { Type: 'String', Value: 'unmatched' },
|
|
116
|
+
Filter2: { Type: 'String', Value: 'filter-2-value-1' }
|
|
117
|
+
})).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('returns false when second filter does not match', async () => {
|
|
121
|
+
const events = await promisedEvents;
|
|
122
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
123
|
+
Filter1: { Type: 'String', Value: 'filter-1-value-1' },
|
|
124
|
+
Filter2: { Type: 'String', Value: 'unmatched' }
|
|
125
|
+
})).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns false when both filters dont match', async () => {
|
|
129
|
+
const events = await promisedEvents;
|
|
130
|
+
return expect(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
131
|
+
Filter1: { Type: 'String', Value: 'unmatched-1' },
|
|
132
|
+
Filter2: { Type: 'String', Value: 'unmatched-2' }
|
|
133
|
+
})).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Resources:
|
|
2
|
+
Unfiltered:
|
|
3
|
+
Type: AWS::Serverless::Function
|
|
4
|
+
Properties:
|
|
5
|
+
Handler: stubHandler
|
|
6
|
+
Events:
|
|
7
|
+
Test:
|
|
8
|
+
Type: SNS
|
|
9
|
+
OrFilter:
|
|
10
|
+
Type: AWS::Serverless::Function
|
|
11
|
+
Properties:
|
|
12
|
+
Handler: stubHandler
|
|
13
|
+
Events:
|
|
14
|
+
Test:
|
|
15
|
+
Type: SNS
|
|
16
|
+
Properties:
|
|
17
|
+
FilterPolicy:
|
|
18
|
+
Filter1:
|
|
19
|
+
- filter-1-value-1
|
|
20
|
+
- filter-1-value-2
|
|
21
|
+
AndFilter:
|
|
22
|
+
Type: AWS::Serverless::Function
|
|
23
|
+
Properties:
|
|
24
|
+
Handler: stubHandler
|
|
25
|
+
Events:
|
|
26
|
+
Test:
|
|
27
|
+
Type: SNS
|
|
28
|
+
Properties:
|
|
29
|
+
FilterPolicy:
|
|
30
|
+
Filter1:
|
|
31
|
+
- filter-1-value-1
|
|
32
|
+
- filter-1-value-2
|
|
33
|
+
Filter2:
|
|
34
|
+
- filter-2-value-1
|
|
35
|
+
- filter-2-value-2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const cloudformation_event_proxy_1 = require("../src/cloudformation-event-proxy");
|
|
5
|
+
const testcontainers_1 = require("testcontainers");
|
|
6
|
+
const SUBJECT_IS_UNUSED = 'anything';
|
|
7
|
+
const emptyDevServerConfig = async () => new testcontainers_1.Network().start().then((network) => ({
|
|
8
|
+
raw: {
|
|
9
|
+
region: "",
|
|
10
|
+
endpoint: "",
|
|
11
|
+
credentials: {
|
|
12
|
+
accessKeyId: "",
|
|
13
|
+
secretAccessKey: ""
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
network: network,
|
|
17
|
+
sqs: undefined,
|
|
18
|
+
sns: undefined,
|
|
19
|
+
s3: undefined,
|
|
20
|
+
dynamo: undefined,
|
|
21
|
+
eventsProxy: {
|
|
22
|
+
topic: {
|
|
23
|
+
arn: ""
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
(0, globals_1.describe)('the cloudformation event proxy', () => {
|
|
28
|
+
(0, globals_1.describe)('the handler matchers', () => {
|
|
29
|
+
const promisedEvents = emptyDevServerConfig().then((config) => (0, cloudformation_event_proxy_1.snsEventsFromCloudFormationTemplate)('test-events', {
|
|
30
|
+
template: () => './__tests__/event-proxy.template',
|
|
31
|
+
handlers: () => '../dist/__tests__/stub-handlers.js',
|
|
32
|
+
handlerNameResolver: (n) => n,
|
|
33
|
+
}).start(config));
|
|
34
|
+
(0, globals_1.describe)('unfiltered event', () => {
|
|
35
|
+
const UNFILTERED_FUNCTION_TEMPLATE_POSITION = 0;
|
|
36
|
+
(0, globals_1.it)('unfiltered event returns true', async () => {
|
|
37
|
+
const events = await promisedEvents;
|
|
38
|
+
return (0, globals_1.expect)(events[UNFILTERED_FUNCTION_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
(0, globals_1.describe)('single attribute filter', () => {
|
|
42
|
+
const SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION = 1;
|
|
43
|
+
globals_1.it.each(['filter-1-value-1', 'filter-1-value-2'])('returns true when matched', async (validFilterValue) => {
|
|
44
|
+
const events = await promisedEvents;
|
|
45
|
+
return (0, globals_1.expect)(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
46
|
+
Filter1: { Type: 'String', Value: validFilterValue }
|
|
47
|
+
})).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
(0, globals_1.it)('returns false when missing', async () => {
|
|
50
|
+
const events = await promisedEvents;
|
|
51
|
+
return (0, globals_1.expect)(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
(0, globals_1.it)('returns false when not matched', async () => {
|
|
54
|
+
const events = await promisedEvents;
|
|
55
|
+
return (0, globals_1.expect)(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
56
|
+
Unexpected: { Type: 'String', Value: 'filter-1-value-1' }
|
|
57
|
+
})).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
(0, globals_1.it)('returns false when not matched', async () => {
|
|
60
|
+
const events = await promisedEvents;
|
|
61
|
+
return (0, globals_1.expect)(events[SINGLE_ATTRIBUTE_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
62
|
+
Filter1: { Type: 'String', Value: 'unexpected-value' }
|
|
63
|
+
})).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
(0, globals_1.describe)('many attributes filter', () => {
|
|
67
|
+
const MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION = 2;
|
|
68
|
+
globals_1.it.each([
|
|
69
|
+
{ Filter1: 'filter-1-value-1', Filter2: 'filter-2-value-1' },
|
|
70
|
+
{ Filter1: 'filter-1-value-1', Filter2: 'filter-2-value-2' },
|
|
71
|
+
{ Filter1: 'filter-1-value-2', Filter2: 'filter-2-value-1' },
|
|
72
|
+
{ Filter1: 'filter-1-value-2', Filter2: 'filter-2-value-2' },
|
|
73
|
+
])('returns true when all matched', async (expectedFilters) => {
|
|
74
|
+
const events = await promisedEvents;
|
|
75
|
+
const filters = Object.entries(expectedFilters).reduce((acc, [key, value]) => {
|
|
76
|
+
acc[key] = {
|
|
77
|
+
Type: 'String',
|
|
78
|
+
Value: value
|
|
79
|
+
};
|
|
80
|
+
return acc;
|
|
81
|
+
}, {});
|
|
82
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, filters)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
(0, globals_1.it)('returns false when both attributes missing', async () => {
|
|
85
|
+
const events = await promisedEvents;
|
|
86
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {})).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
(0, globals_1.it)('returns false when first filter missing', async () => {
|
|
89
|
+
const events = await promisedEvents;
|
|
90
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
91
|
+
Filter2: { Type: 'String', Value: 'filter-2-value-1' }
|
|
92
|
+
})).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
(0, globals_1.it)('returns false when second filter missing', async () => {
|
|
95
|
+
const events = await promisedEvents;
|
|
96
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
97
|
+
Filter1: { Type: 'String', Value: 'filter-2-value-1' }
|
|
98
|
+
})).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
(0, globals_1.it)('returns false when first filter does not match', async () => {
|
|
101
|
+
const events = await promisedEvents;
|
|
102
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
103
|
+
Filter1: { Type: 'String', Value: 'unmatched' },
|
|
104
|
+
Filter2: { Type: 'String', Value: 'filter-2-value-1' }
|
|
105
|
+
})).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
(0, globals_1.it)('returns false when second filter does not match', async () => {
|
|
108
|
+
const events = await promisedEvents;
|
|
109
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
110
|
+
Filter1: { Type: 'String', Value: 'filter-1-value-1' },
|
|
111
|
+
Filter2: { Type: 'String', Value: 'unmatched' }
|
|
112
|
+
})).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
(0, globals_1.it)('returns false when both filters dont match', async () => {
|
|
115
|
+
const events = await promisedEvents;
|
|
116
|
+
return (0, globals_1.expect)(events[MANY_ATTRIBUTES_FILTER_TEMPLATE_POSITION].matcher(SUBJECT_IS_UNUSED, {
|
|
117
|
+
Filter1: { Type: 'String', Value: 'unmatched-1' },
|
|
118
|
+
Filter2: { Type: 'String', Value: 'unmatched-2' }
|
|
119
|
+
})).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -10,11 +10,12 @@ class CloudFormationEventsProxy extends cloudformation_1.CloudFormationSetup {
|
|
|
10
10
|
name: handler.name,
|
|
11
11
|
handler: handler,
|
|
12
12
|
matcher: (_, attributes) => {
|
|
13
|
-
const
|
|
13
|
+
const filters = Object.entries(event.Properties?.FilterPolicy ?? []);
|
|
14
|
+
const required = filters.flatMap((entry) => entry[1].map((value) => value + entry[0]));
|
|
14
15
|
if (required.length == 0)
|
|
15
16
|
return true;
|
|
16
17
|
const actual = Object.entries(attributes).flatMap((entry) => entry[1].Value + entry[0]);
|
|
17
|
-
return required.filter((e) => actual.includes(e)).length
|
|
18
|
+
return required.filter((e) => actual.includes(e)).length == filters.length;
|
|
18
19
|
},
|
|
19
20
|
}), config.prepare, config);
|
|
20
21
|
this.afterStart = (_name, _config, routes) => routes;
|
package/package.json
CHANGED
|
@@ -12,12 +12,6 @@ export const snsEventsFromCloudFormationTemplate = (
|
|
|
12
12
|
new CloudFormationEventsProxy(name, config);
|
|
13
13
|
class CloudFormationEventsProxy extends CloudFormationSetup<
|
|
14
14
|
{
|
|
15
|
-
SqsSubscription: {
|
|
16
|
-
BatchSize: string;
|
|
17
|
-
QueueArn: string;
|
|
18
|
-
QueueUrl: string;
|
|
19
|
-
};
|
|
20
|
-
Topic: string;
|
|
21
15
|
FilterPolicy: {
|
|
22
16
|
[name: string]: string[];
|
|
23
17
|
};
|
|
@@ -34,12 +28,11 @@ class CloudFormationEventsProxy extends CloudFormationSetup<
|
|
|
34
28
|
name: handler.name,
|
|
35
29
|
handler: handler,
|
|
36
30
|
matcher: (_, attributes) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
);
|
|
31
|
+
const filters = Object.entries(event.Properties?.FilterPolicy ?? [])
|
|
32
|
+
const required = filters.flatMap((entry) => entry[1].map((value) => value + entry[0]));
|
|
40
33
|
if (required.length == 0) return true;
|
|
41
34
|
const actual = Object.entries(attributes).flatMap((entry) => entry[1].Value + entry[0]);
|
|
42
|
-
return required.filter((e) => actual.includes(e)).length
|
|
35
|
+
return required.filter((e) => actual.includes(e)).length == filters.length;
|
|
43
36
|
},
|
|
44
37
|
}),
|
|
45
38
|
config.prepare,
|