serverless-plugin-module-registry 1.0.12 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-plugin-module-registry",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "A Serverless Framework plugin that scans module registry files and stores feature mappings in DynamoDB for cross-module discovery",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,6 +50,7 @@
50
50
  "dist/**/*",
51
51
  "src/service.js",
52
52
  "src/service.d.ts",
53
+ "src/internal-infrastructure/**/*",
53
54
  "README.md",
54
55
  "LICENSE"
55
56
  ],
@@ -110,4 +111,4 @@
110
111
  "access": "public",
111
112
  "registry": "https://registry.npmjs.org/"
112
113
  }
113
- }
114
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Custom Stream Enabler Handler Tests
3
+ * Tests for the DynamoDB Streams enablement custom resource handler
4
+ */
5
+
6
+ // Mock AWS SDK before imports
7
+ const mockSend = jest.fn()
8
+ jest.mock('@aws-sdk/client-dynamodb', () => ({
9
+ DynamoDBClient: jest.fn(() => ({
10
+ send: mockSend
11
+ })),
12
+ DescribeTableCommand: jest.fn((input) => input),
13
+ UpdateTableCommand: jest.fn((input) => input)
14
+ }))
15
+
16
+ // Mock https
17
+ const mockHttpsRequest = jest.fn()
18
+ jest.mock('https', () => ({
19
+ request: mockHttpsRequest
20
+ }))
21
+
22
+ // Mock url
23
+ jest.mock('url', () => ({
24
+ parse: jest.fn(() => ({
25
+ hostname: 'test.com',
26
+ path: '/test'
27
+ }))
28
+ }))
29
+
30
+ import { handler } from '../handlers/custom-stream-enabler'
31
+
32
+ describe('Custom Stream Enabler Handler', () => {
33
+ beforeEach(() => {
34
+ jest.clearAllMocks()
35
+ mockSend.mockReset()
36
+ mockHttpsRequest.mockReset()
37
+
38
+ // Setup default https mock
39
+ mockHttpsRequest.mockImplementation((options, callback) => {
40
+ const mockResponse = {}
41
+ if (callback) callback(mockResponse)
42
+ return {
43
+ on: jest.fn(),
44
+ write: jest.fn(),
45
+ end: jest.fn()
46
+ }
47
+ })
48
+ })
49
+
50
+ describe('Create/Update requests', () => {
51
+ it('should enable streams when not already enabled', async () => {
52
+ const event = {
53
+ RequestType: 'Create' as const,
54
+ ResponseURL: 'https://test.com/response',
55
+ StackId: 'stack-123',
56
+ RequestId: 'request-123',
57
+ ResourceType: 'Custom::DynamoDBStreams',
58
+ LogicalResourceId: 'EnableTableStreams',
59
+ ResourceProperties: {
60
+ ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:stream-enabler',
61
+ TableName: 'test-table',
62
+ StreamViewType: 'NEW_AND_OLD_IMAGES' as const
63
+ }
64
+ }
65
+
66
+ const context = {
67
+ logStreamName: 'test-log-stream'
68
+ }
69
+
70
+ // Mock DescribeTable - streams not enabled
71
+ mockSend
72
+ .mockResolvedValueOnce({
73
+ Table: {
74
+ StreamSpecification: {
75
+ StreamEnabled: false
76
+ }
77
+ }
78
+ })
79
+ // Mock UpdateTable
80
+ .mockResolvedValueOnce({})
81
+ // Mock DescribeTable - after update
82
+ .mockResolvedValueOnce({
83
+ Table: {
84
+ LatestStreamArn: 'arn:aws:dynamodb:us-east-1:123456789012:table/test-table/stream/2024-01-01'
85
+ }
86
+ })
87
+
88
+ await handler(event, context)
89
+
90
+ expect(mockSend).toHaveBeenCalledTimes(3)
91
+ expect(mockHttpsRequest).toHaveBeenCalledWith(
92
+ expect.objectContaining({
93
+ method: 'PUT'
94
+ }),
95
+ expect.any(Function)
96
+ )
97
+ })
98
+
99
+ it('should skip enabling streams if already enabled (idempotent)', async () => {
100
+ const event = {
101
+ RequestType: 'Update' as const,
102
+ ResponseURL: 'https://test.com/response',
103
+ StackId: 'stack-123',
104
+ RequestId: 'request-123',
105
+ ResourceType: 'Custom::DynamoDBStreams',
106
+ LogicalResourceId: 'EnableTableStreams',
107
+ ResourceProperties: {
108
+ ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:stream-enabler',
109
+ TableName: 'test-table',
110
+ StreamViewType: 'NEW_AND_OLD_IMAGES' as const
111
+ }
112
+ }
113
+
114
+ const context = {
115
+ logStreamName: 'test-log-stream'
116
+ }
117
+
118
+ // Mock DescribeTable - streams already enabled
119
+ mockSend.mockResolvedValueOnce({
120
+ Table: {
121
+ StreamSpecification: {
122
+ StreamEnabled: true
123
+ },
124
+ LatestStreamArn: 'arn:aws:dynamodb:us-east-1:123456789012:table/test-table/stream/2024-01-01'
125
+ }
126
+ })
127
+
128
+ await handler(event, context)
129
+
130
+ // Should only call DescribeTable once
131
+ expect(mockSend).toHaveBeenCalledTimes(1)
132
+ expect(mockHttpsRequest).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ method: 'PUT'
135
+ }),
136
+ expect.any(Function)
137
+ )
138
+ })
139
+ })
140
+
141
+ describe('Delete requests', () => {
142
+ it('should leave streams enabled on delete for safety', async () => {
143
+ const event = {
144
+ RequestType: 'Delete' as const,
145
+ ResponseURL: 'https://test.com/response',
146
+ StackId: 'stack-123',
147
+ RequestId: 'request-123',
148
+ ResourceType: 'Custom::DynamoDBStreams',
149
+ LogicalResourceId: 'EnableTableStreams',
150
+ PhysicalResourceId: 'test-table',
151
+ ResourceProperties: {
152
+ ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:stream-enabler',
153
+ TableName: 'test-table',
154
+ StreamViewType: 'NEW_AND_OLD_IMAGES' as const
155
+ }
156
+ }
157
+
158
+ const context = {
159
+ logStreamName: 'test-log-stream'
160
+ }
161
+
162
+ await handler(event, context)
163
+
164
+ // Should not call DynamoDB (no disable operation)
165
+ expect(mockSend).not.toHaveBeenCalled()
166
+ expect(mockHttpsRequest).toHaveBeenCalledWith(
167
+ expect.objectContaining({
168
+ method: 'PUT'
169
+ }),
170
+ expect.any(Function)
171
+ )
172
+ })
173
+ })
174
+
175
+ describe('Error handling', () => {
176
+ it('should send FAILED response on error', async () => {
177
+ const event = {
178
+ RequestType: 'Create' as const,
179
+ ResponseURL: 'https://test.com/response',
180
+ StackId: 'stack-123',
181
+ RequestId: 'request-123',
182
+ ResourceType: 'Custom::DynamoDBStreams',
183
+ LogicalResourceId: 'EnableTableStreams',
184
+ ResourceProperties: {
185
+ ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:stream-enabler',
186
+ TableName: 'test-table',
187
+ StreamViewType: 'NEW_AND_OLD_IMAGES' as const
188
+ }
189
+ }
190
+
191
+ const context = {
192
+ logStreamName: 'test-log-stream'
193
+ }
194
+
195
+ // Mock error
196
+ mockSend.mockRejectedValueOnce(new Error('DynamoDB error'))
197
+
198
+ await handler(event, context)
199
+
200
+ expect(mockHttpsRequest).toHaveBeenCalledWith(
201
+ expect.objectContaining({
202
+ method: 'PUT'
203
+ }),
204
+ expect.any(Function)
205
+ )
206
+
207
+ // Verify FAILED status in response body
208
+ const writeCall = mockHttpsRequest.mock.results[0].value.write
209
+ expect(writeCall).toHaveBeenCalledWith(
210
+ expect.stringContaining('"Status":"FAILED"')
211
+ )
212
+ })
213
+ })
214
+ })
@@ -0,0 +1,135 @@
1
+ /**
2
+ * EventBridge Infrastructure Configuration Tests
3
+ *
4
+ * Validates that the EventBridge infrastructure is properly configured
5
+ * in the serverless.yml and resource files.
6
+ */
7
+
8
+ import { readFileSync } from 'fs'
9
+ import { resolve } from 'path'
10
+
11
+ describe('EventBridge Infrastructure Configuration', () => {
12
+ const basePath = resolve(__dirname, '..')
13
+
14
+ let eventbridgeContent: string
15
+ let iamContent: string
16
+ let serverlessContent: string
17
+
18
+ beforeAll(() => {
19
+ // Load resource files as strings to handle CloudFormation intrinsic functions
20
+ eventbridgeContent = readFileSync(
21
+ resolve(basePath, 'resources/eventbridge.yml'),
22
+ 'utf8'
23
+ )
24
+
25
+ iamContent = readFileSync(
26
+ resolve(basePath, 'resources/iam.yml'),
27
+ 'utf8'
28
+ )
29
+
30
+ serverlessContent = readFileSync(
31
+ resolve(basePath, 'serverless.yml'),
32
+ 'utf8'
33
+ )
34
+ })
35
+
36
+ describe('EventBridge EventBus Resource', () => {
37
+ it('should define ModuleRegistryEventBus resource', () => {
38
+ expect(eventbridgeContent).toContain('ModuleRegistryEventBus:')
39
+ expect(eventbridgeContent).toContain('Type: AWS::Events::EventBus')
40
+ })
41
+
42
+ it('should have stage-parameterized name', () => {
43
+ expect(eventbridgeContent).toContain('Name: module-registry-events-${self:provider.stage}')
44
+ })
45
+
46
+ it('should have proper tags', () => {
47
+ expect(eventbridgeContent).toContain('Key: Module')
48
+ expect(eventbridgeContent).toContain('Value: module-registry')
49
+ expect(eventbridgeContent).toContain('Key: Purpose')
50
+ expect(eventbridgeContent).toContain('Value: EventBus')
51
+ expect(eventbridgeContent).toContain('Key: ManagedBy')
52
+ expect(eventbridgeContent).toContain('Value: Serverless')
53
+ })
54
+ })
55
+
56
+ describe('EventBridge CloudFormation Outputs', () => {
57
+ it('should export EventBusArn with proper export name', () => {
58
+ expect(eventbridgeContent).toContain('EventBusArn:')
59
+ expect(eventbridgeContent).toContain('Description: Module Registry EventBridge EventBus ARN')
60
+ expect(eventbridgeContent).toContain('!GetAtt ModuleRegistryEventBus.Arn')
61
+ expect(eventbridgeContent).toContain('Name: ${self:provider.stackName}-EventBusArn')
62
+ })
63
+
64
+ it('should export EventBusName with proper export name', () => {
65
+ expect(eventbridgeContent).toContain('EventBusName:')
66
+ expect(eventbridgeContent).toContain('Description: Module Registry EventBridge EventBus Name')
67
+ expect(eventbridgeContent).toContain('!Ref ModuleRegistryEventBus')
68
+ expect(eventbridgeContent).toContain('Name: ${self:provider.stackName}-EventBusName')
69
+ })
70
+ })
71
+
72
+ describe('StreamProcessor IAM Permissions', () => {
73
+ it('should have events:PutEvents permission', () => {
74
+ expect(iamContent).toContain('StreamProcessorRole:')
75
+ expect(iamContent).toContain('- events:PutEvents')
76
+ })
77
+
78
+ it('should reference ModuleRegistryEventBus ARN', () => {
79
+ expect(iamContent).toContain('!GetAtt ModuleRegistryEventBus.Arn')
80
+ })
81
+
82
+ it('should maintain existing SQS permissions', () => {
83
+ expect(iamContent).toContain('- sqs:SendMessage')
84
+ expect(iamContent).toContain('!GetAtt RoleUpdateQueue.Arn')
85
+ })
86
+
87
+ it('should maintain existing DynamoDB permissions', () => {
88
+ expect(iamContent).toContain('- dynamodb:DescribeStream')
89
+ expect(iamContent).toContain('- dynamodb:GetRecords')
90
+ expect(iamContent).toContain('- dynamodb:GetShardIterator')
91
+ expect(iamContent).toContain('- dynamodb:ListStreams')
92
+ })
93
+ })
94
+
95
+ describe('Serverless Configuration', () => {
96
+ it('should include eventbridge.yml in resources', () => {
97
+ expect(serverlessContent).toContain('${file(resources/eventbridge.yml)}')
98
+ })
99
+
100
+ it('should include eventbridge.yml before iam.yml', () => {
101
+ const eventbridgeIndex = serverlessContent.indexOf('${file(resources/eventbridge.yml)}')
102
+ const iamIndex = serverlessContent.indexOf('${file(resources/iam.yml)}')
103
+
104
+ expect(eventbridgeIndex).toBeGreaterThan(-1)
105
+ expect(iamIndex).toBeGreaterThan(-1)
106
+ expect(eventbridgeIndex).toBeLessThan(iamIndex)
107
+ })
108
+
109
+ it('should configure EVENT_BUS_NAME environment variable', () => {
110
+ expect(serverlessContent).toContain('streamProcessor:')
111
+ expect(serverlessContent).toContain('EVENT_BUS_NAME: !Ref ModuleRegistryEventBus')
112
+ })
113
+
114
+ it('should maintain existing QUEUE_URL environment variable', () => {
115
+ expect(serverlessContent).toContain('QUEUE_URL: !Ref RoleUpdateQueue')
116
+ })
117
+
118
+ it('should maintain existing DynamoDB stream event', () => {
119
+ expect(serverlessContent).toContain('- stream:')
120
+ expect(serverlessContent).toContain('type: dynamodb')
121
+ expect(serverlessContent).toContain('arn: !GetAtt EnableTableStreams.StreamArn')
122
+ })
123
+ })
124
+
125
+ describe('File Existence', () => {
126
+ it('should have eventbridge.yml file', () => {
127
+ expect(eventbridgeContent.length).toBeGreaterThan(0)
128
+ })
129
+
130
+ it('should have valid YAML structure in eventbridge.yml', () => {
131
+ expect(eventbridgeContent).toContain('Resources:')
132
+ expect(eventbridgeContent).toContain('Outputs:')
133
+ })
134
+ })
135
+ })