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/dist/index.d.ts +12 -0
- package/dist/index.js +150 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/internal-infrastructure/__tests__/custom-stream-enabler.test.ts +214 -0
- package/src/internal-infrastructure/__tests__/eventbridge-config.test.ts +135 -0
- package/src/internal-infrastructure/__tests__/role-updater.test.ts +781 -0
- package/src/internal-infrastructure/__tests__/stream-processor.test.ts +994 -0
- package/src/internal-infrastructure/handlers/custom-stream-enabler.ts +163 -0
- package/src/internal-infrastructure/handlers/role-updater.ts +402 -0
- package/src/internal-infrastructure/handlers/shared/logger.ts +48 -0
- package/src/internal-infrastructure/handlers/shared/types.ts +31 -0
- package/src/internal-infrastructure/handlers/stream-processor.ts +371 -0
- package/src/internal-infrastructure/resources/cloudwatch.yml +36 -0
- package/src/internal-infrastructure/resources/custom-stream-enabler.yml +106 -0
- package/src/internal-infrastructure/resources/dynamodb.yml +74 -0
- package/src/internal-infrastructure/resources/eventbridge.yml +26 -0
- package/src/internal-infrastructure/resources/iam.yml +110 -0
- package/src/internal-infrastructure/resources/outputs.yml +13 -0
- package/src/internal-infrastructure/resources/sqs.yml +57 -0
- package/src/internal-infrastructure/serverless.yml +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-plugin-module-registry",
|
|
3
|
-
"version": "1.0.
|
|
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
|
+
})
|