s3-test-server 2.0.0 → 2.1.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.
package/README.md CHANGED
@@ -2,18 +2,12 @@
2
2
 
3
3
  A lightweight Amazon S3 emulator for local development and testing.
4
4
 
5
- Zero runtime dependencies, a single small package, and wire-level
6
- compatibility with both old and new S3 clients:
7
-
8
- - **AWS SDK for JavaScript v3**, including flexible checksums
9
- (CRC32, CRC32C, CRC64NVME, SHA1, SHA256), `aws-chunked` streaming uploads
10
- with trailers, and full-object / composite multipart checksums
11
- - **AWS SDK for JavaScript v2**, SigV4 and legacy SigV2 (`signatureVersion: 's3'`)
12
- - **Any other S3 client** that signs with Signature V2 or V4, header-based or
13
- presigned, path-style or virtual-host-style
14
-
15
- Data is stored on the filesystem (a temp directory by default), so it survives
16
- restarts when you point it at a directory of your own.
5
+ Zero runtime dependencies and wire-level compatibility with old and new S3
6
+ clients: AWS SDK for JavaScript v3 (flexible checksums, `aws-chunked`
7
+ streaming, multipart), v2 (SigV4 and legacy SigV2), and any other client that
8
+ signs with Signature V2/V4, header-based or presigned, path-style or
9
+ virtual-host-style. Objects are stored on the filesystem and persist across
10
+ restarts when you point `directory` at a folder of your own.
17
11
 
18
12
  ## Quick start
19
13
 
@@ -314,6 +308,33 @@ server.on('event', (notification) => {
314
308
  Emitted for `ObjectCreated:Put`, `ObjectCreated:Copy`, `ObjectCreated:Post`,
315
309
  `ObjectCreated:CompleteMultipartUpload` and `ObjectRemoved:Delete`.
316
310
 
311
+ ## Test helpers
312
+
313
+ Three helpers make assertions and fixtures easy without building an S3
314
+ client; they talk to the store directly:
315
+
316
+ ```js
317
+ // seed fixtures (bucket is created if missing; no events are emitted)
318
+ await server.putObjects('reports', [
319
+ { key: 'public/a.csv', body: 'a,b,c', contentType: 'text/csv' },
320
+ { key: 'internal/b.csv', body: Buffer.from('d,e,f') },
321
+ ]);
322
+
323
+ // inspect bucket contents in assertions
324
+ const all = await server.getObjects('reports');
325
+ const csvs = await server.getObjects('reports', {
326
+ prefix: 'public/',
327
+ bodies: true, // each summary gets a `body` Buffer
328
+ });
329
+
330
+ // wait for background uploads from the code under test;
331
+ // multipart uploads only count once completed
332
+ const files = await server.waitForObjects('reports', 2, { timeout: 5000 });
333
+ ```
334
+
335
+ After `run()` resolves, `server.hostPort` (`"localhost:4568"`) and
336
+ `server.endpoint` (`"http://localhost:4568"`) are also set on the instance.
337
+
317
338
  ## Using as middleware
318
339
 
319
340
  ```js
@@ -332,6 +353,9 @@ http.createServer(s3.callback()).listen(4568);
332
353
  - `callback()` / `getMiddleware()` returns a request handler
333
354
  - `configureBuckets()` provisions `options.configureBuckets` without the API
334
355
  - `reset()` deletes all buckets and configurations
356
+ - `getObjects(bucket, { prefix, bodies })` lists completed objects
357
+ - `waitForObjects(bucket, count, { prefix, timeout })` awaits uploads
358
+ - `putObjects(bucket, objects)` seeds fixtures without the HTTP API
335
359
 
336
360
  TypeScript definitions ship with the package (`lib/index.d.ts`).
337
361
 
package/lib/index.d.ts CHANGED
@@ -11,6 +11,12 @@ declare class S3TestServer extends EventEmitter {
11
11
  /** The underlying HTTP(S) server, available after `run()` resolves. */
12
12
  httpServer: HttpServer | null;
13
13
 
14
+ /** `host:port` the server listens on; set once `run()` resolves. */
15
+ hostPort: string | null;
16
+
17
+ /** Full endpoint URL (e.g. `http://localhost:4568`); set once `run()` resolves. */
18
+ endpoint: string | null;
19
+
14
20
  /** Starts the server. */
15
21
  run(): Promise<S3TestServer.RunAddress>;
16
22
  run(
@@ -32,6 +38,34 @@ declare class S3TestServer extends EventEmitter {
32
38
  /** Deletes all buckets, objects and configurations in the store. */
33
39
  reset(): Promise<void>;
34
40
 
41
+ /**
42
+ * Lists the completed objects in a bucket, sorted by key. With
43
+ * `bodies: true`, each summary also carries its content as a Buffer.
44
+ */
45
+ getObjects(
46
+ bucket: string,
47
+ options?: { prefix?: string; bodies?: boolean }
48
+ ): Promise<S3TestServer.ObjectSummary[]>;
49
+
50
+ /**
51
+ * Resolves with the bucket's objects once at least `count` exist
52
+ * (optionally under `prefix`); rejects after `timeout` ms (default 10000).
53
+ */
54
+ waitForObjects(
55
+ bucket: string,
56
+ count: number,
57
+ options?: { prefix?: string; timeout?: number }
58
+ ): Promise<S3TestServer.ObjectSummary[]>;
59
+
60
+ /**
61
+ * Seeds objects directly into the store (the bucket is created when
62
+ * missing). No S3 event notifications are emitted for seeded objects.
63
+ */
64
+ putObjects(
65
+ bucket: string,
66
+ objects: S3TestServer.SeedObject[]
67
+ ): Promise<S3TestServer.ObjectSummary[]>;
68
+
35
69
  on(
36
70
  event: 'event',
37
71
  listener: (notification: S3TestServer.S3EventNotification) => void
@@ -139,11 +173,33 @@ declare namespace S3TestServer {
139
173
  >
140
174
  >;
141
175
 
176
+ interface ObjectSummary {
177
+ key: string;
178
+ size: number;
179
+ /** Hex ETag without quotes. */
180
+ etag: string;
181
+ lastModified: Date;
182
+ contentType?: string;
183
+ /** User metadata (`x-amz-meta-*`), with the prefix stripped. */
184
+ metadata: Record<string, string>;
185
+ /** Present when requested with `bodies: true`. */
186
+ body?: Buffer;
187
+ }
188
+
189
+ interface SeedObject {
190
+ key: string;
191
+ body: string | Buffer;
192
+ contentType?: string;
193
+ metadata?: Record<string, string>;
194
+ }
195
+
142
196
  interface RunAddress {
143
197
  /** Bound address, as reported by `net.Server#address()`. */
144
198
  address: string;
145
199
  family: string;
146
200
  port: number;
201
+ /** `host:port` the server listens on. */
202
+ hostPort: string;
147
203
  /** `"http"` or `"https"`. */
148
204
  protocol: string;
149
205
  /** Convenience endpoint URL, e.g. `"http://localhost:4568"`. */
@@ -198,6 +254,8 @@ declare namespace S3TestServer {
198
254
 
199
255
  export {
200
256
  S3TestServer,
257
+ ObjectSummary,
258
+ SeedObject,
201
259
  CannedBucketAcl,
202
260
  BucketConfiguration,
203
261
  S3TestServerOptions,
package/lib/index.js CHANGED
@@ -52,6 +52,8 @@ class S3TestServer extends EventEmitter {
52
52
  };
53
53
  this.httpServer = null;
54
54
  this.store = null;
55
+ this.hostPort = null;
56
+ this.endpoint = null;
55
57
  this._middleware = null;
56
58
  this._sequencer = 0;
57
59
  }
@@ -110,13 +112,14 @@ class S3TestServer extends EventEmitter {
110
112
  });
111
113
  const address = this.httpServer.address();
112
114
  const protocol = this.options.key && this.options.cert ? 'https' : 'http';
113
- this.logger.info(
114
- `S3 test server listening on ${protocol}://${this.options.address}:${address.port}`
115
- );
115
+ this.hostPort = `${this.options.address}:${address.port}`;
116
+ this.endpoint = `${protocol}://${this.hostPort}`;
117
+ this.logger.info(`S3 test server listening on ${this.endpoint}`);
116
118
  return {
117
119
  ...address,
118
120
  protocol,
119
- endpoint: `${protocol}://${this.options.address}:${address.port}`,
121
+ hostPort: this.hostPort,
122
+ endpoint: this.endpoint,
120
123
  };
121
124
  })();
122
125
  if (typeof callback === 'function') {
@@ -217,6 +220,143 @@ class S3TestServer extends EventEmitter {
217
220
  await this.store.reset();
218
221
  }
219
222
 
223
+ // ---------------------------------------------------------------- helpers
224
+
225
+ _toSummary(meta) {
226
+ return {
227
+ key: meta.key,
228
+ size: meta.size,
229
+ etag: meta.etag,
230
+ lastModified: new Date(meta.lastModified),
231
+ contentType: meta.contentType,
232
+ metadata: { ...(meta.userMetadata || {}) },
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Lists the completed objects in a bucket (multipart parts in flight are
238
+ * never included), sorted by key. With `bodies: true`, each entry also
239
+ * carries its content as a Buffer.
240
+ */
241
+ async getObjects(bucket, { prefix = '', bodies = false } = {}) {
242
+ this._ensureStore();
243
+ if (!(await this.store.getBucket(bucket))) {
244
+ throw new S3Error('NoSuchBucket', 'The specified bucket does not exist', {
245
+ BucketName: bucket,
246
+ });
247
+ }
248
+ const metas = (await this.store.listObjects(bucket)).filter(
249
+ (meta) => !prefix || meta.key.startsWith(prefix)
250
+ );
251
+ const summaries = metas.map((meta) => this._toSummary(meta));
252
+ if (bodies) {
253
+ for (const summary of summaries) {
254
+ summary.body = await this._readObject(bucket, summary.key);
255
+ }
256
+ }
257
+ return summaries;
258
+ }
259
+
260
+ _readObject(bucket, key) {
261
+ return new Promise((resolve, reject) => {
262
+ const chunks = [];
263
+ this.store
264
+ .createObjectReadStream(bucket, key)
265
+ .on('data', (chunk) => chunks.push(chunk))
266
+ .on('end', () => resolve(Buffer.concat(chunks)))
267
+ .on('error', reject);
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Resolves with the bucket's objects once at least `count` of them exist
273
+ * (optionally under `prefix`). Useful when the code under test uploads in
274
+ * the background. A bucket that does not exist yet simply counts as empty.
275
+ */
276
+ async waitForObjects(bucket, count, { prefix = '', timeout = 10000 } = {}) {
277
+ this._ensureStore();
278
+ const matching = async () => {
279
+ if (!(await this.store.getBucket(bucket))) return null;
280
+ const metas = (await this.store.listObjects(bucket)).filter(
281
+ (meta) => !prefix || meta.key.startsWith(prefix)
282
+ );
283
+ return metas.length >= count
284
+ ? metas.map((meta) => this._toSummary(meta))
285
+ : null;
286
+ };
287
+
288
+ const found = await matching();
289
+ if (found) return found;
290
+
291
+ return new Promise((resolve, reject) => {
292
+ let interval;
293
+ let timer;
294
+ const check = async () => {
295
+ const objects = await matching().catch(() => null);
296
+ if (objects) {
297
+ cleanup();
298
+ resolve(objects);
299
+ }
300
+ };
301
+ const cleanup = () => {
302
+ clearInterval(interval);
303
+ clearTimeout(timer);
304
+ this.removeListener('event', check);
305
+ };
306
+ this.on('event', check);
307
+ // Poll as well, so out-of-band writes (e.g. putObjects) are seen too.
308
+ interval = setInterval(check, 150);
309
+ timer = setTimeout(() => {
310
+ cleanup();
311
+ reject(
312
+ new Error(
313
+ `Timed out after ${timeout}ms waiting for ${count} object(s) in "${bucket}"` +
314
+ (prefix ? ` under "${prefix}"` : '')
315
+ )
316
+ );
317
+ }, timeout);
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Seeds objects directly into the store, bypassing the HTTP API. The
323
+ * bucket is created when missing. Each entry is
324
+ * `{ key, body, contentType?, metadata? }` with a string or Buffer body.
325
+ * No S3 event notifications are emitted for seeded objects.
326
+ */
327
+ async putObjects(bucket, objects) {
328
+ this._ensureStore();
329
+ await this.store.initialize();
330
+ const { validateBucketName } = require('./utils');
331
+ validateBucketName(bucket);
332
+ await this.store.createBucket(bucket);
333
+ const { Readable } = require('stream');
334
+ const results = [];
335
+ for (const object of objects) {
336
+ if (!object || typeof object.key !== 'string') {
337
+ throw new TypeError('putObjects entries need a string `key`');
338
+ }
339
+ const body = Buffer.isBuffer(object.body)
340
+ ? object.body
341
+ : Buffer.from(object.body ?? '');
342
+ const staged = await this.store.stageObject(
343
+ bucket,
344
+ Readable.from([body]),
345
+ ['CRC64NVME']
346
+ );
347
+ const meta = await staged.commit(object.key, {
348
+ contentType: object.contentType || 'binary/octet-stream',
349
+ userMetadata: { ...(object.metadata || {}) },
350
+ tagging: {},
351
+ checksums: staged.digests.checksums,
352
+ checksumAlgorithm: 'CRC64NVME',
353
+ checksumType: 'FULL_OBJECT',
354
+ });
355
+ results.push(this._toSummary(meta));
356
+ }
357
+ return results;
358
+ }
359
+
220
360
  _emitEvent(eventName, bucket, objectMeta) {
221
361
  if (!objectMeta) return;
222
362
  this._sequencer += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3-test-server",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A modern, lightweight Amazon S3 emulator for local development and testing. Compatible with AWS SDK v2 and v3, Signature V2/V4, path-style and virtual-host-style addressing, streaming (aws-chunked) uploads and flexible checksums.",
5
5
  "license": "MIT",
6
6
  "author": "Israel Alagbe <israelalagbe53@gmail.com>",