fable 3.1.65 → 3.1.67

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": "fable",
3
- "version": "3.1.65",
3
+ "version": "3.1.67",
4
4
  "description": "A service dependency injection, configuration and logging library.",
5
5
  "main": "source/Fable.js",
6
6
  "scripts": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "homepage": "https://github.com/stevenvelozo/fable",
53
53
  "devDependencies": {
54
- "quackage": "^1.0.59"
54
+ "quackage": "^1.0.65"
55
55
  },
56
56
  "dependencies": {
57
57
  "async.eachlimit": "^0.5.2",
@@ -65,7 +65,7 @@
65
65
  "fable-serviceproviderbase": "^3.0.19",
66
66
  "fable-settings": "^3.0.16",
67
67
  "fable-uuid": "^3.0.13",
68
- "manyfest": "^1.0.48",
68
+ "manyfest": "^1.0.49",
69
69
  "simple-get": "^4.0.1"
70
70
  }
71
71
  }
@@ -42,7 +42,8 @@ class FableServiceExpressionParser extends libFableServiceBase
42
42
  this.log;
43
43
 
44
44
  // The configuration for tokens that the solver recognizes, with precedence and friendly names.
45
- this.tokenMap = require('./Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json');
45
+ // Clone the JSON so each instance gets its own tokenMap (require() caches and returns the same object reference).
46
+ this.tokenMap = JSON.parse(JSON.stringify(require('./Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json')));
46
47
 
47
48
  // Keep track of maximum token precedence
48
49
  this.tokenMaxPrecedence = 4;
@@ -86,7 +87,8 @@ class FableServiceExpressionParser extends libFableServiceBase
86
87
  }
87
88
 
88
89
  // The configuration for which functions are available to the solver.
89
- this.functionMap = require('./Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json');
90
+ // Clone the JSON so each instance gets its own functionMap (require() caches and returns the same object reference).
91
+ this.functionMap = JSON.parse(JSON.stringify(require('./Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json')));
90
92
 
91
93
  this.serviceType = 'ExpressionParser';
92
94
 
@@ -305,6 +305,111 @@ class FableServiceRestClient extends libFableServiceBase
305
305
  return this.executeJSONRequest(pOptions, fCallback);
306
306
  }
307
307
 
308
+ /**
309
+ * Upload binary data via POST.
310
+ *
311
+ * Accepts Buffer, Blob, or File as the body. In the browser, Blob/File
312
+ * bodies are converted to Buffer (via ArrayBuffer) before being passed
313
+ * to simple-get so the stream-http shim can send them correctly.
314
+ *
315
+ * The response body is read as a string (servers typically return JSON
316
+ * status for upload endpoints).
317
+ *
318
+ * @param {Record<string, any>} pOptions - Request options (url, body, headers, method)
319
+ * @param {(pError?: Error, pResponse: any, pBody?: any) => void} fCallback - Callback (pError, pResponse, pBody)
320
+ * @param {(pProgress: number) => void} [fOnProgress] - Optional progress callback (0.0 to 1.0); called with 1.0 on completion
321
+ */
322
+ executeBinaryUpload(pOptions, fCallback, fOnProgress)
323
+ {
324
+ // Blob/File → Buffer conversion for simple-get compatibility
325
+ let tmpBody = pOptions.body;
326
+
327
+ if (typeof Blob !== 'undefined' && tmpBody instanceof Blob)
328
+ {
329
+ let tmpSelf = this;
330
+ tmpBody.arrayBuffer()
331
+ .then(
332
+ (pArrayBuffer) =>
333
+ {
334
+ pOptions.body = Buffer.from(pArrayBuffer);
335
+ tmpSelf._executeBinaryUploadInternal(pOptions, fCallback, fOnProgress);
336
+ })
337
+ .catch(
338
+ (pError) =>
339
+ {
340
+ return fCallback(pError);
341
+ });
342
+ return;
343
+ }
344
+
345
+ // Already a Buffer, string, or stream — proceed directly
346
+ return this._executeBinaryUploadInternal(pOptions, fCallback, fOnProgress);
347
+ }
348
+
349
+ /**
350
+ * Internal binary upload implementation using simple-get.
351
+ *
352
+ * @param {Record<string, any>} pOptions - Request options with body already as Buffer
353
+ * @param {(pError?: Error, pResponse: any, pBody?: any) => void} fCallback - Callback (pError, pResponse, pBody)
354
+ * @param {(pProgress: number) => void} [fOnProgress] - Optional progress callback (0.0 to 1.0); called with 1.0 on completion
355
+ * @private
356
+ */
357
+ _executeBinaryUploadInternal(pOptions, fCallback, fOnProgress)
358
+ {
359
+ let tmpOptions = this.preRequest(pOptions);
360
+
361
+ tmpOptions.RequestStartTime = this.fable.log.getTimeStamp();
362
+
363
+ if (this.TraceLog)
364
+ {
365
+ this.fable.log.debug(`Beginning ${tmpOptions.method} binary upload to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
366
+ }
367
+
368
+ tmpOptions.json = false;
369
+
370
+ return libSimpleGet(tmpOptions,
371
+ (pError, pResponse) =>
372
+ {
373
+ if (pError)
374
+ {
375
+ return fCallback(pError, pResponse);
376
+ }
377
+
378
+ if (this.TraceLog)
379
+ {
380
+ let tmpConnectTime = this.fable.log.getTimeStamp();
381
+ this.fable.log.debug(`--> Binary upload ${tmpOptions.method} connected in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpConnectTime)}ms code ${pResponse.statusCode}`);
382
+ }
383
+
384
+ let tmpData = '';
385
+
386
+ pResponse.on('data', (pChunk) =>
387
+ {
388
+ if (this.TraceLog)
389
+ {
390
+ let tmpChunkTime = this.fable.log.getTimeStamp();
391
+ this.fable.log.debug(`--> Binary upload ${tmpOptions.method} response chunk size ${pChunk.length}b received in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpChunkTime)}ms`);
392
+ }
393
+ tmpData += pChunk;
394
+ });
395
+
396
+ pResponse.on('end', () =>
397
+ {
398
+ if (this.TraceLog)
399
+ {
400
+ let tmpCompletionTime = this.fable.log.getTimeStamp();
401
+ this.fable.log.debug(`==> Binary upload ${tmpOptions.method} completed in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpCompletionTime)}ms`);
402
+ }
403
+ // Signal completion via progress callback
404
+ if (typeof fOnProgress === 'function')
405
+ {
406
+ fOnProgress(1.0);
407
+ }
408
+ return fCallback(pError, pResponse, tmpData);
409
+ });
410
+ });
411
+ }
412
+
308
413
  getRawText(pOptionsOrURL, fCallback)
309
414
  {
310
415
  let tmpRequestOptions = (typeof(pOptionsOrURL) == 'object') ? pOptionsOrURL : {};
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Integration tests for the Fable RestClient executeBinaryUpload method.
3
+ *
4
+ * Spins up a real Orator/Restify server, uploads binary payloads via
5
+ * executeBinaryUpload, and verifies the server received them correctly.
6
+ *
7
+ * @license MIT
8
+ */
9
+
10
+ var libFable = require('../source/Fable.js');
11
+ var libOrator = require('orator');
12
+ var libOratorServiceServerRestify = require('orator-serviceserver-restify');
13
+
14
+ var Chai = require('chai');
15
+ var Expect = Chai.expect;
16
+
17
+ // Port for the test server — pick something unlikely to collide
18
+ var TEST_PORT = 18199;
19
+
20
+ suite
21
+ (
22
+ 'RestClient Binary Upload',
23
+ function ()
24
+ {
25
+ // ----------------------------------------------------------------
26
+ // Test server lifecycle
27
+ // ----------------------------------------------------------------
28
+ var _Fable = null;
29
+ var _Orator = null;
30
+ // Track what the server received on each request
31
+ var _LastReceivedBody = null;
32
+ var _LastReceivedContentType = null;
33
+
34
+ suiteSetup
35
+ (
36
+ function (fDone)
37
+ {
38
+ _Fable = new libFable(
39
+ {
40
+ Product: 'BinaryUploadTestServer',
41
+ APIServerPort: TEST_PORT
42
+ });
43
+
44
+ _Fable.serviceManager.addServiceType('OratorServiceServer', libOratorServiceServerRestify);
45
+
46
+ _Orator = new libOrator(_Fable, {});
47
+
48
+ _Orator.initialize(
49
+ (pError) =>
50
+ {
51
+ if (pError)
52
+ {
53
+ return fDone(pError);
54
+ }
55
+
56
+ // ----------------------------------------------------------
57
+ // POST /1.0/Artifact/Media/:id/:version
58
+ // Receives raw binary body, echoes back metadata as JSON.
59
+ // ----------------------------------------------------------
60
+ _Orator.serviceServer.doPost('/1.0/Artifact/Media/:IDArtifact/:Version',
61
+ (pRequest, pResponse, fNext) =>
62
+ {
63
+ let tmpChunks = [];
64
+
65
+ pRequest.on('data', (pChunk) =>
66
+ {
67
+ tmpChunks.push(pChunk);
68
+ });
69
+
70
+ pRequest.on('end', () =>
71
+ {
72
+ _LastReceivedBody = Buffer.concat(tmpChunks);
73
+ _LastReceivedContentType = pRequest.headers['content-type'] || '';
74
+
75
+ pResponse.setHeader('content-type', 'application/json');
76
+ pResponse.send(200,
77
+ {
78
+ Success: true,
79
+ ReceivedBytes: _LastReceivedBody.length,
80
+ ContentType: _LastReceivedContentType,
81
+ IDArtifact: pRequest.params.IDArtifact,
82
+ Version: pRequest.params.Version
83
+ });
84
+ return fNext();
85
+ });
86
+ });
87
+
88
+ // ----------------------------------------------------------
89
+ // GET /1.0/Artifact/Media/:id/:version
90
+ // Returns a small binary payload for download tests.
91
+ // ----------------------------------------------------------
92
+ _Orator.serviceServer.doGet('/1.0/Artifact/Media/:IDArtifact/:Version',
93
+ (pRequest, pResponse, fNext) =>
94
+ {
95
+ let tmpPayload = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x01, 0x02, 0x03]);
96
+ pResponse.setHeader('content-type', 'image/png');
97
+ pResponse.sendRaw(200, tmpPayload);
98
+ return fNext();
99
+ });
100
+
101
+ // ----------------------------------------------------------
102
+ // POST /1.0/Echo
103
+ // JSON body parser echo endpoint for comparison testing.
104
+ // ----------------------------------------------------------
105
+ _Orator.serviceServer.doPost('/1.0/Echo',
106
+ libOratorServiceServerRestify.prototype.bodyParser.call(_Orator.serviceServer),
107
+ (pRequest, pResponse, fNext) =>
108
+ {
109
+ pResponse.send(200, { Echo: pRequest.body });
110
+ return fNext();
111
+ });
112
+
113
+ _Orator.startService(
114
+ (pStartError) =>
115
+ {
116
+ return fDone(pStartError);
117
+ });
118
+ });
119
+ }
120
+ );
121
+
122
+ suiteTeardown
123
+ (
124
+ function (fDone)
125
+ {
126
+ if (_Orator)
127
+ {
128
+ _Orator.stopService(fDone);
129
+ }
130
+ else
131
+ {
132
+ fDone();
133
+ }
134
+ }
135
+ );
136
+
137
+ setup
138
+ (
139
+ function ()
140
+ {
141
+ _LastReceivedBody = null;
142
+ _LastReceivedContentType = null;
143
+ }
144
+ );
145
+
146
+ // ----------------------------------------------------------------
147
+ // Tests
148
+ // ----------------------------------------------------------------
149
+
150
+ suite
151
+ (
152
+ 'executeBinaryUpload',
153
+ function ()
154
+ {
155
+ test
156
+ (
157
+ 'Upload a Buffer payload and verify server received it.',
158
+ function (fTestComplete)
159
+ {
160
+ let tmpFable = new libFable({ TraceLog: true });
161
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient', { TraceLog: true });
162
+
163
+ let tmpPayload = Buffer.from('Hello binary world! This is a test payload with some bytes.');
164
+ let tmpOptions =
165
+ {
166
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/42/1`,
167
+ method: 'POST',
168
+ body: tmpPayload,
169
+ headers: { 'Content-Type': 'image/jpeg' }
170
+ };
171
+
172
+ tmpRestClient.executeBinaryUpload(tmpOptions,
173
+ (pError, pResponse, pBody) =>
174
+ {
175
+ Expect(pError).to.not.be.an('error');
176
+ Expect(pResponse).to.be.an('object');
177
+ Expect(pResponse.statusCode).to.equal(200);
178
+
179
+ // Parse the JSON response from the server
180
+ let tmpParsedBody = JSON.parse(pBody);
181
+ Expect(tmpParsedBody.Success).to.equal(true);
182
+ Expect(tmpParsedBody.ReceivedBytes).to.equal(tmpPayload.length);
183
+ Expect(tmpParsedBody.ContentType).to.equal('image/jpeg');
184
+ Expect(tmpParsedBody.IDArtifact).to.equal('42');
185
+ Expect(tmpParsedBody.Version).to.equal('1');
186
+
187
+ // Verify the actual bytes the server received
188
+ Expect(_LastReceivedBody).to.be.instanceof(Buffer);
189
+ Expect(_LastReceivedBody.length).to.equal(tmpPayload.length);
190
+ Expect(_LastReceivedBody.toString()).to.equal(tmpPayload.toString());
191
+
192
+ fTestComplete();
193
+ });
194
+ }
195
+ );
196
+
197
+ test
198
+ (
199
+ 'Upload binary data with different MIME type.',
200
+ function (fTestComplete)
201
+ {
202
+ let tmpFable = new libFable();
203
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
204
+
205
+ // Create a binary payload with non-text bytes
206
+ let tmpPayload = Buffer.alloc(256);
207
+ for (let i = 0; i < 256; i++)
208
+ {
209
+ tmpPayload[i] = i;
210
+ }
211
+
212
+ let tmpOptions =
213
+ {
214
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/99/3`,
215
+ method: 'POST',
216
+ body: tmpPayload,
217
+ headers: { 'Content-Type': 'application/pdf' }
218
+ };
219
+
220
+ tmpRestClient.executeBinaryUpload(tmpOptions,
221
+ (pError, pResponse, pBody) =>
222
+ {
223
+ Expect(pError).to.not.be.an('error');
224
+
225
+ let tmpParsedBody = JSON.parse(pBody);
226
+ Expect(tmpParsedBody.Success).to.equal(true);
227
+ Expect(tmpParsedBody.ReceivedBytes).to.equal(256);
228
+ Expect(tmpParsedBody.ContentType).to.equal('application/pdf');
229
+ Expect(tmpParsedBody.IDArtifact).to.equal('99');
230
+ Expect(tmpParsedBody.Version).to.equal('3');
231
+
232
+ // Verify all 256 byte values survived the round-trip
233
+ Expect(_LastReceivedBody.length).to.equal(256);
234
+ for (let i = 0; i < 256; i++)
235
+ {
236
+ Expect(_LastReceivedBody[i]).to.equal(i);
237
+ }
238
+
239
+ fTestComplete();
240
+ });
241
+ }
242
+ );
243
+
244
+ test
245
+ (
246
+ 'Progress callback is called with 1.0 on completion.',
247
+ function (fTestComplete)
248
+ {
249
+ let tmpFable = new libFable();
250
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
251
+
252
+ let tmpPayload = Buffer.from('progress test');
253
+ let tmpProgressCalled = false;
254
+ let tmpProgressValue = -1;
255
+
256
+ let tmpOptions =
257
+ {
258
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/1/1`,
259
+ method: 'POST',
260
+ body: tmpPayload,
261
+ headers: { 'Content-Type': 'application/octet-stream' }
262
+ };
263
+
264
+ tmpRestClient.executeBinaryUpload(tmpOptions,
265
+ (pError, pResponse, pBody) =>
266
+ {
267
+ Expect(pError).to.not.be.an('error');
268
+ Expect(tmpProgressCalled).to.equal(true);
269
+ Expect(tmpProgressValue).to.equal(1.0);
270
+ fTestComplete();
271
+ },
272
+ (pProgress) =>
273
+ {
274
+ tmpProgressCalled = true;
275
+ tmpProgressValue = pProgress;
276
+ });
277
+ }
278
+ );
279
+
280
+ test
281
+ (
282
+ 'Upload empty body returns success.',
283
+ function (fTestComplete)
284
+ {
285
+ let tmpFable = new libFable();
286
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
287
+
288
+ let tmpPayload = Buffer.alloc(0);
289
+ let tmpOptions =
290
+ {
291
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/5/1`,
292
+ method: 'POST',
293
+ body: tmpPayload,
294
+ headers: { 'Content-Type': 'image/png' }
295
+ };
296
+
297
+ tmpRestClient.executeBinaryUpload(tmpOptions,
298
+ (pError, pResponse, pBody) =>
299
+ {
300
+ Expect(pError).to.not.be.an('error');
301
+ let tmpParsedBody = JSON.parse(pBody);
302
+ Expect(tmpParsedBody.Success).to.equal(true);
303
+ Expect(tmpParsedBody.ReceivedBytes).to.equal(0);
304
+ fTestComplete();
305
+ });
306
+ }
307
+ );
308
+
309
+ test
310
+ (
311
+ 'Upload a large binary payload (1MB).',
312
+ function (fTestComplete)
313
+ {
314
+ this.timeout(10000);
315
+
316
+ let tmpFable = new libFable();
317
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
318
+
319
+ // Create a 1MB payload with a known pattern
320
+ let tmpSize = 1024 * 1024;
321
+ let tmpPayload = Buffer.alloc(tmpSize);
322
+ for (let i = 0; i < tmpSize; i++)
323
+ {
324
+ tmpPayload[i] = i % 256;
325
+ }
326
+
327
+ let tmpOptions =
328
+ {
329
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/100/1`,
330
+ method: 'POST',
331
+ body: tmpPayload,
332
+ headers: { 'Content-Type': 'application/octet-stream' }
333
+ };
334
+
335
+ tmpRestClient.executeBinaryUpload(tmpOptions,
336
+ (pError, pResponse, pBody) =>
337
+ {
338
+ Expect(pError).to.not.be.an('error');
339
+
340
+ let tmpParsedBody = JSON.parse(pBody);
341
+ Expect(tmpParsedBody.Success).to.equal(true);
342
+ Expect(tmpParsedBody.ReceivedBytes).to.equal(tmpSize);
343
+
344
+ // Verify first and last bytes
345
+ Expect(_LastReceivedBody[0]).to.equal(0);
346
+ Expect(_LastReceivedBody[255]).to.equal(255);
347
+ Expect(_LastReceivedBody[256]).to.equal(0);
348
+ Expect(_LastReceivedBody[tmpSize - 1]).to.equal((tmpSize - 1) % 256);
349
+
350
+ fTestComplete();
351
+ });
352
+ }
353
+ );
354
+
355
+ test
356
+ (
357
+ 'Works with RestClientURLPrefix setting.',
358
+ function (fTestComplete)
359
+ {
360
+ let tmpFable = new libFable(
361
+ {
362
+ RestClientURLPrefix: `http://localhost:${TEST_PORT}`
363
+ });
364
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
365
+
366
+ let tmpPayload = Buffer.from('prefix test');
367
+ let tmpOptions =
368
+ {
369
+ url: '/1.0/Artifact/Media/7/2',
370
+ method: 'POST',
371
+ body: tmpPayload,
372
+ headers: { 'Content-Type': 'text/plain' }
373
+ };
374
+
375
+ tmpRestClient.executeBinaryUpload(tmpOptions,
376
+ (pError, pResponse, pBody) =>
377
+ {
378
+ Expect(pError).to.not.be.an('error');
379
+
380
+ let tmpParsedBody = JSON.parse(pBody);
381
+ Expect(tmpParsedBody.Success).to.equal(true);
382
+ Expect(tmpParsedBody.IDArtifact).to.equal('7');
383
+ Expect(tmpParsedBody.Version).to.equal('2');
384
+ Expect(tmpParsedBody.ContentType).to.equal('text/plain');
385
+
386
+ fTestComplete();
387
+ });
388
+ }
389
+ );
390
+
391
+ test
392
+ (
393
+ 'Cookies are applied via prepareCookies.',
394
+ function (fTestComplete)
395
+ {
396
+ let tmpFable = new libFable();
397
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
398
+
399
+ tmpRestClient.cookie = { SessionID: 'abc123' };
400
+
401
+ let tmpPayload = Buffer.from('cookie test');
402
+ let tmpOptions =
403
+ {
404
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/8/1`,
405
+ method: 'POST',
406
+ body: tmpPayload,
407
+ headers: { 'Content-Type': 'image/jpeg' }
408
+ };
409
+
410
+ tmpRestClient.executeBinaryUpload(tmpOptions,
411
+ (pError, pResponse, pBody) =>
412
+ {
413
+ Expect(pError).to.not.be.an('error');
414
+
415
+ let tmpParsedBody = JSON.parse(pBody);
416
+ Expect(tmpParsedBody.Success).to.equal(true);
417
+
418
+ fTestComplete();
419
+ });
420
+ }
421
+ );
422
+ }
423
+ );
424
+
425
+ suite
426
+ (
427
+ 'executeChunkedRequest binary download',
428
+ function ()
429
+ {
430
+ test
431
+ (
432
+ 'Download binary data via GET.',
433
+ function (fTestComplete)
434
+ {
435
+ let tmpFable = new libFable({ TraceLog: true });
436
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient', { TraceLog: true });
437
+
438
+ tmpRestClient.executeChunkedRequest(
439
+ {
440
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/42/1`,
441
+ method: 'GET'
442
+ },
443
+ (pError, pResponse, pBody) =>
444
+ {
445
+ Expect(pError).to.not.be.an('error');
446
+ Expect(pResponse.statusCode).to.equal(200);
447
+ // Body is a string from executeChunkedRequest
448
+ Expect(pBody.length).to.be.greaterThan(0);
449
+ fTestComplete();
450
+ });
451
+ }
452
+ );
453
+
454
+ test
455
+ (
456
+ 'Download binary data via executeChunkedRequestBinary returns Buffer.',
457
+ function (fTestComplete)
458
+ {
459
+ let tmpFable = new libFable();
460
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
461
+
462
+ tmpRestClient.executeChunkedRequestBinary(
463
+ {
464
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/42/1`,
465
+ method: 'GET'
466
+ },
467
+ (pError, pResponse, pBuffer) =>
468
+ {
469
+ Expect(pError).to.not.be.an('error');
470
+ Expect(pResponse.statusCode).to.equal(200);
471
+ Expect(pBuffer).to.be.instanceof(Buffer);
472
+ // Check PNG magic bytes
473
+ Expect(pBuffer[0]).to.equal(0x89);
474
+ Expect(pBuffer[1]).to.equal(0x50);
475
+ Expect(pBuffer[2]).to.equal(0x4E);
476
+ Expect(pBuffer[3]).to.equal(0x47);
477
+ fTestComplete();
478
+ });
479
+ }
480
+ );
481
+ }
482
+ );
483
+
484
+ suite
485
+ (
486
+ 'Round-trip: upload then download',
487
+ function ()
488
+ {
489
+ test
490
+ (
491
+ 'Upload binary, then download and verify content matches.',
492
+ function (fTestComplete)
493
+ {
494
+ let tmpFable = new libFable();
495
+ let tmpRestClient = tmpFable.instantiateServiceProvider('RestClient');
496
+
497
+ // Upload
498
+ let tmpPayload = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]);
499
+ let tmpOptions =
500
+ {
501
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/200/1`,
502
+ method: 'POST',
503
+ body: tmpPayload,
504
+ headers: { 'Content-Type': 'application/octet-stream' }
505
+ };
506
+
507
+ tmpRestClient.executeBinaryUpload(tmpOptions,
508
+ (pUploadError, pUploadResponse, pUploadBody) =>
509
+ {
510
+ Expect(pUploadError).to.not.be.an('error');
511
+
512
+ let tmpParsedBody = JSON.parse(pUploadBody);
513
+ Expect(tmpParsedBody.Success).to.equal(true);
514
+ Expect(tmpParsedBody.ReceivedBytes).to.equal(6);
515
+
516
+ // Now download from the same endpoint
517
+ tmpRestClient.executeChunkedRequestBinary(
518
+ {
519
+ url: `http://localhost:${TEST_PORT}/1.0/Artifact/Media/200/1`,
520
+ method: 'GET'
521
+ },
522
+ (pDownloadError, pDownloadResponse, pBuffer) =>
523
+ {
524
+ Expect(pDownloadError).to.not.be.an('error');
525
+ Expect(pBuffer).to.be.instanceof(Buffer);
526
+ // The download endpoint returns a fixed PNG stub,
527
+ // not the uploaded data — but both should succeed
528
+ Expect(pBuffer.length).to.be.greaterThan(0);
529
+ fTestComplete();
530
+ });
531
+ });
532
+ }
533
+ );
534
+ }
535
+ );
536
+ }
537
+ );