@gugananuvem/aws-local-simulator 1.0.22 → 1.0.26
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/README.md +45 -0
- package/aws-config +154 -0
- package/package.json +30 -84
- package/src/config/default-config.js +15 -4
- package/src/server.js +257 -21
- package/src/services/apigateway/server.js +37 -0
- package/src/services/apigateway/simulator.js +148 -4
- package/src/services/cognito/server.js +8 -14
- package/src/services/cognito/simulator.js +63 -11
- package/src/services/dynamodb/simulator.js +159 -49
- package/src/services/kms/server.js +15 -1
- package/src/services/kms/simulator.js +48 -28
- package/src/services/lambda/server.js +24 -0
- package/src/services/lambda/simulator.js +136 -12
- package/src/services/parameter-store/simulator.js +1 -1
- package/src/services/s3/server.js +21 -0
- package/src/services/s3/simulator.js +4 -1
- package/src/services/secret-manager/server.js +2 -1
- package/src/services/secret-manager/simulator.js +21 -10
- package/src/services/sns/server.js +32 -5
- package/src/services/sqs/server.js +11 -0
- package/src/services/sqs/simulator.js +74 -6
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const LocalStore = require("../../utils/local-store");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
5
4
|
const HandlerLoader = require("./handler-loader");
|
|
6
5
|
const logger = require("../../utils/logger");
|
|
7
6
|
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
@@ -9,6 +8,8 @@ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
|
9
8
|
class LambdaSimulator {
|
|
10
9
|
constructor(config) {
|
|
11
10
|
this.config = config;
|
|
11
|
+
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "lambda");
|
|
12
|
+
this.store = new LocalStore(this.dataDir);
|
|
12
13
|
this.lambdas = new Map(); // functionName -> { handler, env, config }
|
|
13
14
|
this.environment = { ...process.env };
|
|
14
15
|
this.audit = new CloudTrailAudit("lambda.amazonaws.com");
|
|
@@ -23,15 +24,27 @@ class LambdaSimulator {
|
|
|
23
24
|
this.globalTimeout = globalDefaults.timeout || null;
|
|
24
25
|
this.globalMemorySize = globalDefaults.memorySize || null;
|
|
25
26
|
|
|
27
|
+
// Carrega do config.lambdas (fixo)
|
|
26
28
|
if (this.config.lambdas && this.config.lambdas.length > 0) {
|
|
27
29
|
for (const lambdaConfig of this.config.lambdas) {
|
|
28
30
|
await this.registerLambda(lambdaConfig);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
// Carrega lambdas dinâmicas do disco
|
|
35
|
+
const savedLambdas = this.store.read("__functions__");
|
|
36
|
+
if (savedLambdas && Array.isArray(savedLambdas)) {
|
|
37
|
+
for (const lambdaConfig of savedLambdas) {
|
|
38
|
+
if (!this.lambdas.has(lambdaConfig.name)) {
|
|
39
|
+
await this.registerLambda(lambdaConfig);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
logger.debug(`✅ ${this.lambdas.size} Lambdas registradas`);
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
|
|
35
48
|
async registerLambda(lambdaConfig) {
|
|
36
49
|
try {
|
|
37
50
|
const { name, handler: handlerPath, type = "auto" } = lambdaConfig;
|
|
@@ -51,6 +64,14 @@ class LambdaSimulator {
|
|
|
51
64
|
const memorySize = lambdaConfig.memorySize ?? this.globalMemorySize ?? 128;
|
|
52
65
|
|
|
53
66
|
const handler = await HandlerLoader.load(handlerPath, type);
|
|
67
|
+
let codeSize = 0;
|
|
68
|
+
try {
|
|
69
|
+
const info = await HandlerLoader.getInfo(handlerPath);
|
|
70
|
+
codeSize = info.size;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Ignore size errors
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
if (handler != undefined) {
|
|
55
76
|
this.lambdas.set(name, {
|
|
56
77
|
name,
|
|
@@ -60,6 +81,7 @@ class LambdaSimulator {
|
|
|
60
81
|
env,
|
|
61
82
|
timeout,
|
|
62
83
|
memorySize,
|
|
84
|
+
codeSize,
|
|
63
85
|
type,
|
|
64
86
|
registeredAt: new Date().toISOString(),
|
|
65
87
|
});
|
|
@@ -147,16 +169,32 @@ class LambdaSimulator {
|
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
listLambdas() {
|
|
150
|
-
return Array.from(this.lambdas.values()).map((l) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
return Array.from(this.lambdas.values()).map((l) => {
|
|
173
|
+
let code = "";
|
|
174
|
+
try {
|
|
175
|
+
if (fs.existsSync(l.handlerPath)) {
|
|
176
|
+
code = fs.readFileSync(l.handlerPath, "utf8");
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.error(`Erro ao ler código da lambda ${l.name}:`, err);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name: l.name,
|
|
184
|
+
handlerName: l.handlerName,
|
|
185
|
+
handlerPath: l.handlerPath,
|
|
186
|
+
type: l.type,
|
|
187
|
+
env: l.env,
|
|
188
|
+
timeout: l.timeout,
|
|
189
|
+
memorySize: l.memorySize,
|
|
190
|
+
codeSize: l.codeSize,
|
|
191
|
+
registeredAt: l.registeredAt,
|
|
192
|
+
code: code
|
|
193
|
+
};
|
|
194
|
+
});
|
|
158
195
|
}
|
|
159
196
|
|
|
197
|
+
|
|
160
198
|
getLambda(name) {
|
|
161
199
|
return this.lambdas.get(name);
|
|
162
200
|
}
|
|
@@ -182,6 +220,91 @@ class LambdaSimulator {
|
|
|
182
220
|
logger.info(`✅ ${this.lambdas.size} Lambdas recarregadas`);
|
|
183
221
|
}
|
|
184
222
|
|
|
223
|
+
async createFunction(lambdaConfig) {
|
|
224
|
+
const { name, code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
|
|
225
|
+
|
|
226
|
+
if (!name) throw new Error("Function name is required");
|
|
227
|
+
|
|
228
|
+
// Define o caminho do arquivo (dentro do dataDir/functions)
|
|
229
|
+
const functionsDir = path.join(this.dataDir, "functions");
|
|
230
|
+
if (!fs.existsSync(functionsDir)) fs.mkdirSync(functionsDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const fileName = `${name}.js`;
|
|
233
|
+
const filePath = path.join(functionsDir, fileName);
|
|
234
|
+
|
|
235
|
+
// Salva o código no disco
|
|
236
|
+
fs.writeFileSync(filePath, code || "// Hello Lambda");
|
|
237
|
+
|
|
238
|
+
// Registra a lambda
|
|
239
|
+
const config = {
|
|
240
|
+
name,
|
|
241
|
+
handler: filePath,
|
|
242
|
+
runtime: runtime || "nodejs18.x",
|
|
243
|
+
timeout: timeout || 30,
|
|
244
|
+
memorySize: memorySize || 128,
|
|
245
|
+
env: environment || {},
|
|
246
|
+
type: "commonjs"
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await this.registerLambda(config);
|
|
250
|
+
this.persistLambdas();
|
|
251
|
+
|
|
252
|
+
return this.lambdas.get(name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async updateFunction(name, lambdaConfig) {
|
|
256
|
+
const lambda = this.lambdas.get(name);
|
|
257
|
+
if (!lambda) throw new Error(`Function not found: ${name}`);
|
|
258
|
+
|
|
259
|
+
const { code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
|
|
260
|
+
|
|
261
|
+
// Se houver código novo, sobrescreve o arquivo
|
|
262
|
+
if (code !== undefined) {
|
|
263
|
+
fs.writeFileSync(lambda.handlerPath, code);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Atualiza a configuração
|
|
267
|
+
const updatedConfig = {
|
|
268
|
+
name,
|
|
269
|
+
handler: lambda.handlerPath,
|
|
270
|
+
runtime: runtime || lambda.runtime,
|
|
271
|
+
timeout: timeout || lambda.timeout,
|
|
272
|
+
memorySize: memorySize || lambda.memorySize,
|
|
273
|
+
env: environment || lambda.env,
|
|
274
|
+
type: lambda.type
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
await this.registerLambda(updatedConfig);
|
|
278
|
+
this.persistLambdas();
|
|
279
|
+
|
|
280
|
+
return this.lambdas.get(name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async deleteFunction(name) {
|
|
284
|
+
|
|
285
|
+
if (this.lambdas.has(name)) {
|
|
286
|
+
this.lambdas.delete(name);
|
|
287
|
+
this.persistLambdas();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
persistLambdas() {
|
|
294
|
+
const functionsToSave = Array.from(this.lambdas.values())
|
|
295
|
+
.filter(l => l.handlerPath.includes(this.dataDir)) // Apenas as dinâmicas
|
|
296
|
+
.map(l => ({
|
|
297
|
+
name: l.name,
|
|
298
|
+
handler: l.handlerPath,
|
|
299
|
+
type: l.type,
|
|
300
|
+
env: l.env,
|
|
301
|
+
timeout: l.timeout,
|
|
302
|
+
memorySize: l.memorySize,
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
this.store.write("__functions__", functionsToSave);
|
|
306
|
+
}
|
|
307
|
+
|
|
185
308
|
getStats() {
|
|
186
309
|
return {
|
|
187
310
|
totalLambdas: this.lambdas.size,
|
|
@@ -189,6 +312,7 @@ class LambdaSimulator {
|
|
|
189
312
|
};
|
|
190
313
|
}
|
|
191
314
|
|
|
315
|
+
|
|
192
316
|
async reset() {
|
|
193
317
|
await this.reloadLambdas();
|
|
194
318
|
this.environment = { ...process.env };
|
|
@@ -50,7 +50,7 @@ class ParameterStoreSimulator {
|
|
|
50
50
|
const storedValue = Type === 'SecureString' ? this._encrypt(Value) : Value;
|
|
51
51
|
const param = {
|
|
52
52
|
Name, Value: storedValue, Type, Description: Description || '', Version: version,
|
|
53
|
-
LastModifiedDate:
|
|
53
|
+
LastModifiedDate: Math.floor(Date.now() / 1000),
|
|
54
54
|
LastModifiedUser: 'local',
|
|
55
55
|
ARN: `arn:aws:ssm:local:000000000000:parameter${Name}`,
|
|
56
56
|
DataType, Tags: Tags || [], KeyId: Type === 'SecureString' ? (KeyId || 'aws/ssm') : undefined
|
|
@@ -23,6 +23,26 @@ class S3Server {
|
|
|
23
23
|
this.app.use(express.raw({ type: () => true, limit: '100mb' }));
|
|
24
24
|
this.app.use(express.text({ limit: '100mb' }));
|
|
25
25
|
|
|
26
|
+
// Suporte a Virtual Host Style addressing (bucket.localhost:4566)
|
|
27
|
+
this.app.use((req, res, next) => {
|
|
28
|
+
const host = req.headers.host || '';
|
|
29
|
+
// Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
|
|
30
|
+
if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
31
|
+
const parts = host.split('.');
|
|
32
|
+
// O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
|
|
33
|
+
// ou se a segunda parte for localhost
|
|
34
|
+
if (parts.length >= 2) {
|
|
35
|
+
const bucket = parts[0];
|
|
36
|
+
// Evita processar se for admin ou se já estiver no path style (heurística simples)
|
|
37
|
+
if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
|
|
38
|
+
req.url = `/${bucket}${req.url}`;
|
|
39
|
+
logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
45
|
+
|
|
26
46
|
// Logging de requisições
|
|
27
47
|
if (logger.currentLogLevel === 'verboso') {
|
|
28
48
|
this.app.use((req, res, next) => {
|
|
@@ -36,6 +56,7 @@ class S3Server {
|
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
|
|
39
60
|
async initialize() {
|
|
40
61
|
if (!this.simulator) {
|
|
41
62
|
this.simulator = new S3Simulator(this.config);
|
|
@@ -489,10 +489,13 @@ class S3Simulator {
|
|
|
489
489
|
// ─── Utilitários ──────────────────────────────────────────────────────────
|
|
490
490
|
|
|
491
491
|
isValidBucketName(bucketName) {
|
|
492
|
-
|
|
492
|
+
// S3 oficial é rigoroso, mas para simulador local vamos ser mais tolerantes
|
|
493
|
+
// permitindo underscores e letras maiúsculas que são comuns em testes locais
|
|
494
|
+
const regex = /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,61}[a-zA-Z0-9]$/;
|
|
493
495
|
return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
|
|
494
496
|
}
|
|
495
497
|
|
|
498
|
+
|
|
496
499
|
extractMetadata(headers) {
|
|
497
500
|
const metadata = {};
|
|
498
501
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -37,7 +37,8 @@ class SecretManagerServer {
|
|
|
37
37
|
if (!operation) return res.status(400).json({ __type: 'UnknownOperationException', message: `Unknown: ${target}` });
|
|
38
38
|
try {
|
|
39
39
|
const result = await this.simulator[operation](req.body || {});
|
|
40
|
-
res.
|
|
40
|
+
res.setHeader('Content-Type', 'application/x-amz-json-1.1');
|
|
41
|
+
res.send(JSON.stringify(result || {}));
|
|
41
42
|
} catch (err) {
|
|
42
43
|
this.logger.error(`SecretsManager ${target}: ${err.message}`, 'secret-manager');
|
|
43
44
|
res.status(err.code === 'ResourceNotFoundException' ? 404 : 400).json({ __type: err.code || 'InternalServiceError', Message: err.message });
|
|
@@ -17,7 +17,18 @@ class SecretManagerSimulator {
|
|
|
17
17
|
async initialize() {
|
|
18
18
|
try {
|
|
19
19
|
const secrets = await this.store.read('secret-manager/secrets');
|
|
20
|
-
if (Array.isArray(secrets))
|
|
20
|
+
if (Array.isArray(secrets)) {
|
|
21
|
+
for (const s of secrets) {
|
|
22
|
+
if (typeof s.CreatedDate === 'string') s.CreatedDate = Math.floor(new Date(s.CreatedDate).getTime() / 1000);
|
|
23
|
+
if (typeof s.LastChangedDate === 'string') s.LastChangedDate = Math.floor(new Date(s.LastChangedDate).getTime() / 1000);
|
|
24
|
+
if (s._versions) {
|
|
25
|
+
for (const v of Object.values(s._versions)) {
|
|
26
|
+
if (typeof v.CreatedDate === 'string') v.CreatedDate = Math.floor(new Date(v.CreatedDate).getTime() / 1000);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.secrets.set(s.Name, s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
21
32
|
this.logger.info('SecretsManager: dados carregados', 'secret-manager');
|
|
22
33
|
} catch { this.logger.debug('SecretsManager: sem dados anteriores', 'secret-manager'); }
|
|
23
34
|
}
|
|
@@ -38,12 +49,12 @@ class SecretManagerSimulator {
|
|
|
38
49
|
ARN: `arn:aws:secretsmanager:local:000000000000:secret:${Name}-${secretId.slice(0, 6)}`,
|
|
39
50
|
Name, Description: Description || '', Tags,
|
|
40
51
|
KmsKeyId: KmsKeyId || 'aws/secretsmanager',
|
|
41
|
-
CreatedDate:
|
|
42
|
-
LastChangedDate:
|
|
52
|
+
CreatedDate: Math.floor(Date.now() / 1000),
|
|
53
|
+
LastChangedDate: Math.floor(Date.now() / 1000),
|
|
43
54
|
LastAccessedDate: null,
|
|
44
55
|
RotationEnabled: false,
|
|
45
56
|
VersionsToStages: { [secretId]: ['AWSCURRENT'] },
|
|
46
|
-
_versions: { [secretId]: { SecretString, SecretBinary, CreatedDate:
|
|
57
|
+
_versions: { [secretId]: { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) } }
|
|
47
58
|
};
|
|
48
59
|
this.secrets.set(Name, secret);
|
|
49
60
|
await this._persist();
|
|
@@ -55,7 +66,7 @@ class SecretManagerSimulator {
|
|
|
55
66
|
async getSecretValue(params) {
|
|
56
67
|
const { SecretId, VersionId, VersionStage = 'AWSCURRENT' } = params;
|
|
57
68
|
const secret = this._requireSecret(SecretId);
|
|
58
|
-
secret.LastAccessedDate =
|
|
69
|
+
secret.LastAccessedDate = Math.floor(Date.now() / 1000);
|
|
59
70
|
let versionId = VersionId;
|
|
60
71
|
if (!versionId) {
|
|
61
72
|
versionId = Object.entries(secret.VersionsToStages).find(([, stages]) => stages.includes(VersionStage))?.[0];
|
|
@@ -81,9 +92,9 @@ class SecretManagerSimulator {
|
|
|
81
92
|
secret.VersionsToStages[vid] = stages.filter(s => s !== 'AWSCURRENT').concat(['AWSPREVIOUS']);
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
|
-
secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate:
|
|
95
|
+
secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) };
|
|
85
96
|
secret.VersionsToStages[versionId] = VersionStages;
|
|
86
|
-
secret.LastChangedDate =
|
|
97
|
+
secret.LastChangedDate = Math.floor(Date.now() / 1000);
|
|
87
98
|
await this._persist();
|
|
88
99
|
return { ARN: secret.ARN, Name: secret.Name, VersionId: versionId, VersionStages };
|
|
89
100
|
}
|
|
@@ -103,8 +114,8 @@ class SecretManagerSimulator {
|
|
|
103
114
|
async deleteSecret(params) {
|
|
104
115
|
const { SecretId, RecoveryWindowInDays = 30, ForceDeleteWithoutRecovery } = params;
|
|
105
116
|
const secret = this._requireSecret(SecretId);
|
|
106
|
-
const deletionDate = ForceDeleteWithoutRecovery ?
|
|
107
|
-
secret.DeletedDate =
|
|
117
|
+
const deletionDate = ForceDeleteWithoutRecovery ? Math.floor(Date.now() / 1000) : Math.floor((Date.now() + RecoveryWindowInDays * 86400000) / 1000);
|
|
118
|
+
secret.DeletedDate = Math.floor(Date.now() / 1000);
|
|
108
119
|
secret.DeletionDate = deletionDate;
|
|
109
120
|
if (ForceDeleteWithoutRecovery) this.secrets.delete(secret.Name);
|
|
110
121
|
await this._persist();
|
|
@@ -146,7 +157,7 @@ class SecretManagerSimulator {
|
|
|
146
157
|
secret.RotationEnabled = true;
|
|
147
158
|
secret.RotationLambdaARN = RotationLambdaARN;
|
|
148
159
|
secret.RotationRules = RotationRules;
|
|
149
|
-
secret.LastRotatedDate =
|
|
160
|
+
secret.LastRotatedDate = Math.floor(Date.now() / 1000);
|
|
150
161
|
await this._persist();
|
|
151
162
|
return { ARN: secret.ARN, Name: secret.Name };
|
|
152
163
|
}
|
|
@@ -81,27 +81,39 @@ function createSNSServer(simulator, config, logger) {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
app.get('/__admin/topics', (_req, res) => {
|
|
84
|
-
res.json(
|
|
84
|
+
res.json(Array.from(simulator.topics.values()));
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
app.post('/__admin/topics', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const topic = await simulator.createTopic({ Name: req.body.name });
|
|
90
|
+
res.status(201).json(topic);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
res.status(400).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
87
97
|
app.get('/__admin/subscriptions', (_req, res) => {
|
|
88
|
-
res.json(
|
|
98
|
+
res.json(Array.from(simulator.subscriptions.values()));
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
app.get('/__admin/subscriptions/:topicName', (req, res) => {
|
|
92
102
|
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
93
103
|
const subs = Array.from(simulator.subscriptions.values()).filter(s => s.TopicArn === arn);
|
|
94
|
-
res.json(
|
|
104
|
+
res.json(subs);
|
|
95
105
|
});
|
|
96
106
|
|
|
107
|
+
|
|
97
108
|
app.get('/__admin/publish-log', (_req, res) => {
|
|
98
|
-
res.json(
|
|
109
|
+
res.json(simulator.publishLog);
|
|
99
110
|
});
|
|
100
111
|
|
|
101
112
|
app.get('/__admin/platform-apps', (_req, res) => {
|
|
102
|
-
res.json(
|
|
113
|
+
res.json(Array.from(simulator.platformApps.values()));
|
|
103
114
|
});
|
|
104
115
|
|
|
116
|
+
|
|
105
117
|
app.delete('/__admin/topics/:topicName', async (req, res) => {
|
|
106
118
|
try {
|
|
107
119
|
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
@@ -112,6 +124,21 @@ function createSNSServer(simulator, config, logger) {
|
|
|
112
124
|
}
|
|
113
125
|
});
|
|
114
126
|
|
|
127
|
+
app.post('/__admin/topics/:topicName/publish', async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
130
|
+
const result = await simulator.publish({
|
|
131
|
+
TopicArn: arn,
|
|
132
|
+
Subject: req.body.subject,
|
|
133
|
+
Message: req.body.message
|
|
134
|
+
});
|
|
135
|
+
res.json(result);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
res.status(400).json({ error: err.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
|
|
115
142
|
app.post('/__admin/reset', async (_req, res) => {
|
|
116
143
|
await simulator.reset();
|
|
117
144
|
res.json({ message: 'SNS data reset complete' });
|
|
@@ -187,7 +187,10 @@ class SQSServer {
|
|
|
187
187
|
return this.generateReceiveMessageResponse(result.messages);
|
|
188
188
|
case 'DeleteMessage':
|
|
189
189
|
return this.generateDeleteMessageResponse();
|
|
190
|
+
case 'SetQueueAttributes':
|
|
191
|
+
return this.generateSetQueueAttributesResponse();
|
|
190
192
|
case 'GetQueueUrl':
|
|
193
|
+
|
|
191
194
|
return this.generateGetQueueUrlResponse(result.queueUrl);
|
|
192
195
|
case 'ListQueues':
|
|
193
196
|
return this.generateListQueuesResponse(result.queues);
|
|
@@ -196,6 +199,14 @@ class SQSServer {
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
generateSetQueueAttributesResponse() {
|
|
203
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
204
|
+
<SetQueueAttributesResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
|
|
205
|
+
<ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
|
|
206
|
+
</SetQueueAttributesResponse>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
199
210
|
generateCreateQueueResponse(queueUrl) {
|
|
200
211
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
201
212
|
<CreateQueueResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
|
|
@@ -49,7 +49,7 @@ class SQSSimulator {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
createQueue(queueName) {
|
|
52
|
+
createQueue(queueName, attributes = {}) {
|
|
53
53
|
if (this.queues.has(queueName)) {
|
|
54
54
|
return { error: { code: 'QueueAlreadyExists', message: 'Queue already exists' }, status: 409 };
|
|
55
55
|
}
|
|
@@ -60,12 +60,18 @@ class SQSSimulator {
|
|
|
60
60
|
arn: `arn:aws:sqs:local:000000000000:${queueName}`,
|
|
61
61
|
messages: [],
|
|
62
62
|
handler: null,
|
|
63
|
-
batchSize: 10,
|
|
64
|
-
visibilityTimeout: 30,
|
|
63
|
+
batchSize: parseInt(attributes.BatchSize || 10),
|
|
64
|
+
visibilityTimeout: parseInt(attributes.VisibilityTimeout || 30),
|
|
65
|
+
delaySeconds: parseInt(attributes.DelaySeconds || 0),
|
|
66
|
+
messageRetentionPeriod: parseInt(attributes.MessageRetentionPeriod || 345600),
|
|
65
67
|
createdAt: new Date().toISOString(),
|
|
66
68
|
messageCount: 0
|
|
67
69
|
};
|
|
68
70
|
|
|
71
|
+
if (attributes.LambdaName) {
|
|
72
|
+
this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
this.queues.set(queueName, queue);
|
|
70
76
|
this.persistQueues();
|
|
71
77
|
// Only initialize message store if it doesn't already exist
|
|
@@ -73,7 +79,7 @@ class SQSSimulator {
|
|
|
73
79
|
this.store.write(queueName, []);
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
logger.debug(`✅ Fila SQS criada: ${queueName}`);
|
|
82
|
+
logger.debug(`✅ Fila SQS criada: ${queueName} com atributos: ${JSON.stringify(attributes)}`);
|
|
77
83
|
|
|
78
84
|
return { queue };
|
|
79
85
|
}
|
|
@@ -82,6 +88,8 @@ class SQSSimulator {
|
|
|
82
88
|
switch(action) {
|
|
83
89
|
case 'CreateQueue':
|
|
84
90
|
return this.createQueueAction(req);
|
|
91
|
+
case 'SetQueueAttributes':
|
|
92
|
+
return this.setQueueAttributesAction(req);
|
|
85
93
|
case 'SendMessage':
|
|
86
94
|
return this.sendMessageAction(req);
|
|
87
95
|
case 'SendMessageBatch':
|
|
@@ -108,9 +116,26 @@ class SQSSimulator {
|
|
|
108
116
|
return undefined;
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
// Parses Attribute.N.Name and Attribute.N.Value from request body/query
|
|
120
|
+
extractAttributes(req) {
|
|
121
|
+
const attributes = {};
|
|
122
|
+
const source = req.body.Action ? req.body : req.query;
|
|
123
|
+
|
|
124
|
+
Object.keys(source).forEach(key => {
|
|
125
|
+
if (key.startsWith('Attribute.') && key.endsWith('.Name')) {
|
|
126
|
+
const index = key.split('.')[1];
|
|
127
|
+
const name = source[key];
|
|
128
|
+
const value = source[`Attribute.${index}.Value`];
|
|
129
|
+
attributes[name] = value;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return attributes;
|
|
133
|
+
}
|
|
134
|
+
|
|
111
135
|
createQueueAction(req) {
|
|
112
136
|
const queueName = req.query.QueueName || req.body.QueueName;
|
|
113
|
-
const
|
|
137
|
+
const attributes = this.extractAttributes(req);
|
|
138
|
+
const result = this.createQueue(queueName, attributes);
|
|
114
139
|
|
|
115
140
|
if (result.error) {
|
|
116
141
|
return result;
|
|
@@ -119,6 +144,39 @@ class SQSSimulator {
|
|
|
119
144
|
return { queueUrl: result.queue.url };
|
|
120
145
|
}
|
|
121
146
|
|
|
147
|
+
setQueueAttributesAction(req) {
|
|
148
|
+
const queueName = this.resolveQueueName(req);
|
|
149
|
+
const queue = this.queues.get(queueName);
|
|
150
|
+
|
|
151
|
+
if (!queue) {
|
|
152
|
+
return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const attributes = this.extractAttributes(req);
|
|
156
|
+
|
|
157
|
+
if (attributes.DelaySeconds !== undefined) queue.delaySeconds = parseInt(attributes.DelaySeconds);
|
|
158
|
+
if (attributes.VisibilityTimeout !== undefined) queue.visibilityTimeout = parseInt(attributes.VisibilityTimeout);
|
|
159
|
+
if (attributes.MessageRetentionPeriod !== undefined) queue.messageRetentionPeriod = parseInt(attributes.MessageRetentionPeriod);
|
|
160
|
+
|
|
161
|
+
if (attributes.LambdaName !== undefined) {
|
|
162
|
+
queue.lambdaName = attributes.LambdaName;
|
|
163
|
+
if (attributes.LambdaName) {
|
|
164
|
+
// Here we'd normally re-attach if we had a direct reference to the lambda service function
|
|
165
|
+
// For now, we store the name and the server/index.js will handle re-attachment or we use the name to look it up
|
|
166
|
+
this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
|
|
167
|
+
} else {
|
|
168
|
+
queue.handler = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (attributes.BatchSize !== undefined) {
|
|
173
|
+
queue.batchSize = parseInt(attributes.BatchSize);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.persistQueues();
|
|
177
|
+
return { success: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
sendMessageAction(req) {
|
|
123
181
|
const queueName = this.resolveQueueName(req);
|
|
124
182
|
const queue = this.queues.get(queueName);
|
|
@@ -334,9 +392,13 @@ class SQSSimulator {
|
|
|
334
392
|
arn: queue.arn,
|
|
335
393
|
batchSize: queue.batchSize,
|
|
336
394
|
visibilityTimeout: queue.visibilityTimeout,
|
|
395
|
+
delaySeconds: queue.delaySeconds,
|
|
396
|
+
messageRetentionPeriod: queue.messageRetentionPeriod,
|
|
397
|
+
lambdaName: queue.lambdaName,
|
|
337
398
|
createdAt: queue.createdAt,
|
|
338
399
|
messageCount: queue.messageCount
|
|
339
400
|
};
|
|
401
|
+
|
|
340
402
|
}
|
|
341
403
|
this.store.write('__queues__', queuesObj);
|
|
342
404
|
}
|
|
@@ -372,10 +434,16 @@ class SQSSimulator {
|
|
|
372
434
|
name: q.name,
|
|
373
435
|
url: q.url,
|
|
374
436
|
messagesCount: q.messageCount,
|
|
375
|
-
createdAt: q.createdAt
|
|
437
|
+
createdAt: q.createdAt,
|
|
438
|
+
delaySeconds: q.delaySeconds,
|
|
439
|
+
visibilityTimeout: q.visibilityTimeout,
|
|
440
|
+
messageRetentionPeriod: q.messageRetentionPeriod,
|
|
441
|
+
lambdaName: q.lambdaName,
|
|
442
|
+
batchSize: q.batchSize
|
|
376
443
|
}));
|
|
377
444
|
}
|
|
378
445
|
|
|
446
|
+
|
|
379
447
|
getQueue(queueName) {
|
|
380
448
|
const queue = this.queues.get(queueName);
|
|
381
449
|
if (!queue) return null;
|