scratch-storage 2.1.0 → 2.2.1

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,13 +1,13 @@
1
1
  {
2
2
  "name": "scratch-storage",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Load and store project and asset files for Scratch 3.0",
5
5
  "license": "BSD-3-Clause",
6
6
  "homepage": "https://github.com/LLK/scratch-storage#readme",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LLK/scratch-storage.git",
10
- "sha": "2f6a562d46c4393cc70ab8c1b4350beaf6c07675"
10
+ "sha": "4294057468737f2e179afe2cc963326d20942aed"
11
11
  },
12
12
  "main": "./dist/node/scratch-storage.js",
13
13
  "browser": "./src/index.js",
@@ -24,9 +24,14 @@
24
24
  "watch": "webpack --progress --colors --watch",
25
25
  "semantic-release": "semantic-release"
26
26
  },
27
+ "tap": {
28
+ "check-coverage": false
29
+ },
27
30
  "dependencies": {
31
+ "@babel/runtime": "7.21.0",
28
32
  "arraybuffer-loader": "^1.0.3",
29
33
  "base64-js": "1.3.0",
34
+ "cross-fetch": "3.1.5",
30
35
  "fastestsmallesttextencoderdecoder": "^1.0.7",
31
36
  "js-md5": "0.7.3",
32
37
  "minilog": "3.1.0",
@@ -34,6 +39,7 @@
34
39
  },
35
40
  "devDependencies": {
36
41
  "@babel/core": "7.14.8",
42
+ "@babel/plugin-transform-runtime": "7.21.0",
37
43
  "@babel/polyfill": "7.12.1",
38
44
  "@babel/preset-env": "7.14.8",
39
45
  "@commitlint/cli": "8.2.0",
@@ -48,9 +54,8 @@
48
54
  "file-loader": "4.1.0",
49
55
  "husky": "1.3.1",
50
56
  "json": "^9.0.4",
51
- "node-fetch": "2.6.1",
52
57
  "semantic-release": "^15.10.5",
53
- "tap": "12.1.1",
58
+ "tap": "16.3.4",
54
59
  "uglifyjs-webpack-plugin": "2.2.0",
55
60
  "webpack": "4.46.0",
56
61
  "webpack-cli": "3.1.2"
package/src/FetchTool.js CHANGED
@@ -1,48 +1,52 @@
1
- /* eslint-env browser */
1
+ const {scratchFetch} = require('./scratchFetch');
2
+
3
+ /**
4
+ * @typedef {Request & {withCredentials: boolean}} ScratchSendRequest
5
+ */
2
6
 
3
7
  /**
4
8
  * Get and send assets with the fetch standard web api.
5
9
  */
6
10
  class FetchTool {
7
11
  /**
8
- * Is get supported? false if the environment does not support fetch.
12
+ * Is get supported?
13
+ * Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
9
14
  * @returns {boolean} Is get supported?
10
15
  */
11
16
  get isGetSupported () {
12
- return typeof fetch !== 'undefined';
17
+ return true;
13
18
  }
14
19
 
15
20
  /**
16
21
  * Request data from a server with fetch.
17
- * @param {{url:string}} reqConfig - Request configuration for data to get.
18
- * @param {{method:string}} options - Additional options to configure fetch.
19
- * @returns {Promise.<Uint8Array>} Resolve to Buffer of data from server.
22
+ * @param {Request} reqConfig - Request configuration for data to get.
23
+ * @returns {Promise.<Uint8Array?>} Resolve to Buffer of data from server.
20
24
  */
21
25
  get ({url, ...options}) {
22
- return fetch(url, Object.assign({method: 'GET'}, options))
26
+ return scratchFetch(url, Object.assign({method: 'GET'}, options))
23
27
  .then(result => {
24
28
  if (result.ok) return result.arrayBuffer().then(b => new Uint8Array(b));
25
29
  if (result.status === 404) return null;
26
- return Promise.reject(result.status);
30
+ return Promise.reject(result.status); // TODO: we should throw a proper error
27
31
  });
28
32
  }
29
33
 
30
34
  /**
31
- * Is sending supported? false if the environment does not support sending
32
- * with fetch.
35
+ * Is sending supported?
36
+ * Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
33
37
  * @returns {boolean} Is sending supported?
34
38
  */
35
39
  get isSendSupported () {
36
- return typeof fetch !== 'undefined';
40
+ return true;
37
41
  }
38
42
 
39
43
  /**
40
44
  * Send data to a server with fetch.
41
- * @param {Request} reqConfig - Request configuration for data to send.
45
+ * @param {ScratchSendRequest} reqConfig - Request configuration for data to send.
42
46
  * @returns {Promise.<string>} Server returned metadata.
43
47
  */
44
48
  send ({url, withCredentials = false, ...options}) {
45
- return fetch(url, Object.assign({
49
+ return scratchFetch(url, Object.assign({
46
50
  credentials: withCredentials ? 'include' : 'omit'
47
51
  }, options))
48
52
  .then(response => {
@@ -1,3 +1,5 @@
1
+ const {Headers, applyMetadata} = require('./scratchFetch');
2
+
1
3
  /**
2
4
  * Get and send assets with a worker that uses fetch.
3
5
  */
@@ -13,13 +15,13 @@ class PrivateFetchWorkerTool {
13
15
 
14
16
  /**
15
17
  * A possible error occurred standing up the worker.
16
- * @type {!Error}
18
+ * @type {Error?}
17
19
  */
18
20
  this._supportError = null;
19
21
 
20
22
  /**
21
23
  * The worker that runs fetch and returns data for us.
22
- * @type {!Worker}
24
+ * @type {Worker?}
23
25
  */
24
26
  this.worker = null;
25
27
 
@@ -34,9 +36,9 @@ class PrivateFetchWorkerTool {
34
36
  // eslint-disable-next-line global-require
35
37
  const FetchWorker = require('worker-loader?{"inline":true,"fallback":true}!./FetchWorkerTool.worker');
36
38
 
37
- this.worker = new FetchWorker();
39
+ const worker = new FetchWorker();
38
40
 
39
- this.worker.addEventListener('message', ({data}) => {
41
+ worker.addEventListener('message', ({data}) => {
40
42
  if (data.support) {
41
43
  this._workerSupport = data.support;
42
44
  return;
@@ -52,6 +54,8 @@ class PrivateFetchWorkerTool {
52
54
  }
53
55
  }
54
56
  });
57
+
58
+ this.worker = worker;
55
59
  }
56
60
  } catch (error) {
57
61
  this._supportError = error;
@@ -78,17 +82,27 @@ class PrivateFetchWorkerTool {
78
82
  * Request data from a server with a worker using fetch.
79
83
  * @param {{url:string}} reqConfig - Request configuration for data to get.
80
84
  * @param {{method:string}} options - Additional options to configure fetch.
81
- * @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
85
+ * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
82
86
  */
83
87
  get ({url, ...options}) {
84
88
  return new Promise((resolve, reject) => {
85
89
  // TODO: Use a Scratch standard ID generator ...
86
90
  const id = Math.random().toString(16)
87
91
  .substring(2);
92
+ const augmentedOptions = applyMetadata(
93
+ Object.assign({method: 'GET'}, options)
94
+ );
95
+ // the Fetch spec says options.headers could be:
96
+ // "A Headers object, an object literal, or an array of two-item arrays to set request's headers."
97
+ // structured clone (postMessage) doesn't support Headers objects
98
+ // so turn it into an array of two-item arrays to make it to the worker intact
99
+ if (augmentedOptions && augmentedOptions.headers instanceof Headers) {
100
+ augmentedOptions.headers = Array.from(augmentedOptions.headers.entries());
101
+ }
88
102
  this.worker.postMessage({
89
103
  id,
90
104
  url,
91
- options: Object.assign({method: 'GET'}, options)
105
+ options: augmentedOptions
92
106
  });
93
107
  this.jobs[id] = {
94
108
  id,
@@ -153,7 +167,7 @@ class PublicFetchWorkerTool {
153
167
  /**
154
168
  * Request data from a server with a worker that uses fetch.
155
169
  * @param {{url:string}} reqConfig - Request configuration for data to get.
156
- * @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
170
+ * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
157
171
  */
158
172
  get (reqConfig) {
159
173
  return this.inner.get(reqConfig);
@@ -1,5 +1,7 @@
1
1
  /* eslint-env worker */
2
2
 
3
+ const crossFetch = require('cross-fetch').default;
4
+
3
5
  let jobsActive = 0;
4
6
  const complete = [];
5
7
 
@@ -48,7 +50,7 @@ const onMessage = ({data: job}) => {
48
50
 
49
51
  jobsActive++;
50
52
 
51
- fetch(job.url, job.options)
53
+ crossFetch(job.url, job.options)
52
54
  .then(result => {
53
55
  if (result.ok) return result.arrayBuffer();
54
56
  if (result.status === 404) return null;
@@ -59,12 +61,6 @@ const onMessage = ({data: job}) => {
59
61
  .then(() => jobsActive--);
60
62
  };
61
63
 
62
- if (self.fetch) {
63
- postMessage({support: {fetch: true}});
64
- self.addEventListener('message', onMessage);
65
- } else {
66
- postMessage({support: {fetch: false}});
67
- self.addEventListener('message', ({data: job}) => {
68
- postMessage([{id: job.id, error: 'fetch is unavailable'}]);
69
- });
70
- }
64
+ // crossFetch means "fetch" is now always supported
65
+ postMessage({support: {fetch: true}});
66
+ self.addEventListener('message', onMessage);
@@ -6,6 +6,7 @@ const WebHelper = require('./WebHelper');
6
6
  const _Asset = require('./Asset');
7
7
  const _AssetType = require('./AssetType');
8
8
  const _DataFormat = require('./DataFormat');
9
+ const _scratchFetch = require('./scratchFetch');
9
10
 
10
11
  class ScratchStorage {
11
12
  constructor () {
@@ -51,6 +52,14 @@ class ScratchStorage {
51
52
  return _DataFormat;
52
53
  }
53
54
 
55
+ /**
56
+ * Access the `scratchFetch` module within this library.
57
+ * @return {module} the scratchFetch module, with properties for `scratchFetch`, `setMetadata`, etc.
58
+ */
59
+ get scratchFetch () {
60
+ return _scratchFetch;
61
+ }
62
+
54
63
  /**
55
64
  * @deprecated Please use the `Asset` member of a storage instance instead.
56
65
  * @return {Asset} - the `Asset` class constructor.
@@ -0,0 +1,113 @@
1
+ const crossFetch = require('cross-fetch');
2
+
3
+ /**
4
+ * Metadata header names
5
+ * @enum {string} The enum value is the name of the associated header.
6
+ * @readonly
7
+ */
8
+ const RequestMetadata = {
9
+ /** The ID of the project associated with this request */
10
+ ProjectId: 'X-Project-ID',
11
+ /** The ID of the project run associated with this request */
12
+ RunId: 'X-Run-ID'
13
+ };
14
+
15
+ /**
16
+ * Metadata headers for requests
17
+ * @type {Headers}
18
+ */
19
+ const metadata = new crossFetch.Headers();
20
+
21
+ /**
22
+ * Check if there is any metadata to apply.
23
+ * @returns {boolean} true if `metadata` has contents, or false if it is empty.
24
+ */
25
+ const hasMetadata = () => {
26
+ for (const _ of metadata) {
27
+ return true;
28
+ }
29
+ return false;
30
+ };
31
+
32
+ /**
33
+ * Non-destructively merge any metadata state (if any) with the provided options object (if any).
34
+ * If there is metadata state but no options object is provided, make a new object.
35
+ * If there is no metadata state, return the provided options parameter without modification.
36
+ * If there is metadata and an options object is provided, modify a copy and return it.
37
+ * Headers in the provided options object may override headers generated from metadata state.
38
+ * @param {RequestInit} [options] The initial request options. May be null or undefined.
39
+ * @returns {RequestInit|undefined} the provided options parameter without modification, or a new options object.
40
+ */
41
+ const applyMetadata = options => {
42
+ if (hasMetadata()) {
43
+ const augmentedOptions = Object.assign({}, options);
44
+ augmentedOptions.headers = new crossFetch.Headers(metadata);
45
+ if (options && options.headers) {
46
+ // the Fetch spec says options.headers could be:
47
+ // "A Headers object, an object literal, or an array of two-item arrays to set request's headers."
48
+ // turn it into a Headers object to be sure of how to interact with it
49
+ const overrideHeaders = options.headers instanceof crossFetch.Headers ?
50
+ options.headers : new crossFetch.Headers(options.headers);
51
+ for (const [name, value] of overrideHeaders.entries()) {
52
+ augmentedOptions.headers.set(name, value);
53
+ }
54
+ }
55
+ return augmentedOptions;
56
+ }
57
+ return options;
58
+ };
59
+
60
+ /**
61
+ * Make a network request.
62
+ * This is a wrapper for the global fetch method, adding some Scratch-specific functionality.
63
+ * @param {RequestInfo|URL} resource The resource to fetch.
64
+ * @param {RequestInit} options Optional object containing custom settings for this request.
65
+ * @see {@link https://developer.mozilla.org/docs/Web/API/fetch} for more about the fetch API.
66
+ * @returns {Promise<Response>} A promise for the response to the request.
67
+ */
68
+ const scratchFetch = (resource, options) => {
69
+ const augmentedOptions = applyMetadata(options);
70
+ return crossFetch.fetch(resource, augmentedOptions);
71
+ };
72
+
73
+ /**
74
+ * Set the value of a named request metadata item.
75
+ * Setting the value to `null` or `undefined` will NOT remove the item.
76
+ * Use `unsetMetadata` for that.
77
+ * @param {RequestMetadata} name The name of the metadata item to set.
78
+ * @param {any} value The value to set (will be converted to a string).
79
+ */
80
+ const setMetadata = (name, value) => {
81
+ metadata.set(name, value);
82
+ };
83
+
84
+ /**
85
+ * Remove a named request metadata item.
86
+ * @param {RequestMetadata} name The name of the metadata item to remove.
87
+ */
88
+ const unsetMetadata = name => {
89
+ metadata.delete(name);
90
+ };
91
+
92
+ module.exports = {
93
+ default: scratchFetch,
94
+
95
+ Headers: crossFetch.Headers,
96
+ RequestMetadata,
97
+ applyMetadata,
98
+ scratchFetch,
99
+ setMetadata,
100
+ unsetMetadata
101
+ };
102
+
103
+ if (process.env.NODE_ENV === 'development') {
104
+ /**
105
+ * Retrieve a named request metadata item.
106
+ * Only for use in tests.
107
+ * @param {RequestMetadata} name The name of the metadata item to retrieve.
108
+ * @returns {any} value The value of the metadata item, or `undefined` if it was not found.
109
+ */
110
+ const getMetadata = name => metadata.get(name);
111
+
112
+ module.exports.getMetadata = getMetadata;
113
+ }
@@ -0,0 +1,69 @@
1
+ const TextEncoder = require('util').TextEncoder;
2
+ const crossFetch = require('cross-fetch');
3
+
4
+ const Headers = crossFetch.Headers;
5
+ const successText = 'successful response';
6
+
7
+ /**
8
+ * @typedef MockFetchResponse The Response-like object returned by mockFetch.
9
+ * @property {boolean} ok True if the simulated request was successful, false otherwise.
10
+ * @property {number} status The HTTP status code of the simulated request.
11
+ * @property {() => Promise<string>} [text] A success string if the simulated request succeeded, undefined otherwise.
12
+ * @property {() => Promise<Uint8Array>} [arrayBuffer] Same as `text`, but encoded with UTF-8 if present.
13
+ */
14
+
15
+ /**
16
+ * @typedef {RequestInit & {mockFetchTestData: MockFetchTestData}} MockFetchRequestInit
17
+ */
18
+
19
+ /**
20
+ * @typedef MockFetchTestData
21
+ * @property {Headers} [headers] A Headers object initialized with the header info received by mockFetch.
22
+ * @property {Number} [headersCount] The number of headers in the 'headers' property.
23
+ */
24
+
25
+ /**
26
+ * Mock the 'fetch' method from browsers.
27
+ * @param {RequestInfo|URL} resource The (mock) resource to fetch, which will determine the response.
28
+ * @param {MockFetchRequestInit} [options] Optional object containing custom settings for this request.
29
+ * @returns {Promise<MockFetchResponse>} A promise for a Response-like object. Does not fully implement Response.
30
+ */
31
+ const mockFetch = (resource, options) => {
32
+ /** @type MockFetchResponse */
33
+ const results = {
34
+ ok: false,
35
+ status: 0
36
+ };
37
+ if (options?.mockFetchTestData) {
38
+ options.mockFetchTestData.headers = new Headers(options.headers);
39
+ options.mockFetchTestData.headersCount = Array.from(options.mockFetchTestData.headers).length;
40
+ }
41
+ switch (resource) {
42
+ case '200':
43
+ results.ok = true;
44
+ results.status = 200;
45
+ results.text = () => Promise.resolve(successText);
46
+ results.arrayBuffer = () => Promise.resolve(new TextEncoder().encode(successText));
47
+ break;
48
+ case '404':
49
+ results.ok = false;
50
+ results.status = 404;
51
+ break;
52
+ case '500':
53
+ results.ok = false;
54
+ results.status = 500;
55
+ break;
56
+ default:
57
+ throw new Error('unimplemented');
58
+ }
59
+ return Promise.resolve(results);
60
+ };
61
+
62
+ // Mimic the cross-fetch module, but replace its `fetch` with `mockFetch` and add a few extras
63
+ module.exports = {
64
+ ...crossFetch, // Headers, Request, Response, etc.
65
+ default: mockFetch,
66
+ fetch: mockFetch,
67
+ mockFetch,
68
+ successText
69
+ };
@@ -1,77 +1,57 @@
1
- const test = require('tap').test;
2
- const TextEncoder = require('util').TextEncoder;
1
+ const tap = require('tap');
3
2
  const TextDecoder = require('util').TextDecoder;
4
3
 
5
- const FetchTool = require('../../src/FetchTool');
4
+ const mockFetch = require('../mocks/mock-fetch.js');
6
5
 
7
- test('send success returns response.text()', t => {
8
- global.fetch = () => Promise.resolve({
9
- ok: true,
10
- text: () => Promise.resolve('successful response')
11
- });
6
+ /**
7
+ * This is the real FetchTool, but the 'cross-fetch' module has been replaced with the mockFetch function.
8
+ * @type {typeof import('../../src/FetchTool')}
9
+ */
10
+ const FetchTool = tap.mock('../../src/FetchTool', {
11
+ 'cross-fetch': mockFetch
12
+ });
12
13
 
14
+ tap.test('send success returns response.text()', t => {
13
15
  const tool = new FetchTool();
14
-
16
+
15
17
  return t.resolves(
16
- tool.send('url').then(result => {
17
- t.equal(result, 'successful response');
18
+ tool.send({url: '200'}).then(result => {
19
+ t.equal(result, mockFetch.successText);
18
20
  })
19
21
  );
20
22
  });
21
23
 
22
- test('send failure returns response.status', t => {
23
- global.fetch = () => Promise.resolve({
24
- ok: false,
25
- status: 500
26
- });
27
-
24
+ tap.test('send failure returns response.status', t => {
28
25
  const tool = new FetchTool();
29
26
 
30
- return t.rejects(tool.send('url'), 500);
27
+ return t.rejects(tool.send({url: '500'}), 500);
31
28
  });
32
29
 
33
- test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
34
- const text = 'successful response';
30
+ tap.test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
35
31
  const encoding = 'utf-8';
36
- const encoded = new TextEncoder().encode(text);
37
32
  const decoder = new TextDecoder(encoding);
38
33
 
39
- global.fetch = () => Promise.resolve({
40
- ok: true,
41
- arrayBuffer: () => Promise.resolve(encoded.buffer)
42
- });
43
-
44
34
  const tool = new FetchTool();
45
-
35
+
46
36
  return t.resolves(
47
- tool.get({url: 'url'}).then(result => {
48
- t.equal(decoder.decode(result), text);
37
+ tool.get({url: '200'}).then(result => {
38
+ t.equal(decoder.decode(result), mockFetch.successText);
49
39
  })
50
40
  );
51
41
  });
52
42
 
53
- test('get with 404 response returns null data', t => {
54
- global.fetch = () => Promise.resolve({
55
- ok: false,
56
- status: 404
57
- });
58
-
43
+ tap.test('get with 404 response returns null data', t => {
59
44
  const tool = new FetchTool();
60
45
 
61
46
  return t.resolves(
62
- tool.get('url').then(result => {
47
+ tool.get({url: '404'}).then(result => {
63
48
  t.equal(result, null);
64
49
  })
65
50
  );
66
51
  });
67
52
 
68
- test('get failure returns response.status', t => {
69
- global.fetch = () => Promise.resolve({
70
- ok: false,
71
- status: 500
72
- });
73
-
53
+ tap.test('get failure returns response.status', t => {
74
54
  const tool = new FetchTool();
75
55
 
76
- return t.rejects(tool.get({url: 'url'}), 500);
56
+ return t.rejects(tool.get({url: '500'}), 500);
77
57
  });
@@ -0,0 +1,147 @@
1
+ const tap = require('tap');
2
+
3
+ const mockFetchModule = require('../mocks/mock-fetch.js');
4
+
5
+
6
+ // Call this separately from each test to ensure that metadata gets reset.
7
+ // This is especially important when parallelizing tests!
8
+ const setupModules = () => {
9
+ /**
10
+ * This instance of scratchFetch will be shared between this file and FetchTool.
11
+ * By sharing the same instance, the test can affect the metadata that FetchTool will use.
12
+ */
13
+ const scratchFetchModule = tap.mock('../../src/scratchFetch', {
14
+ 'cross-fetch': mockFetchModule
15
+ });
16
+
17
+ /**
18
+ * This is the real FetchTool, but the 'cross-fetch' module has been replaced with the mockFetch function.
19
+ * @type {typeof import('../../src/FetchTool')}
20
+ */
21
+ const FetchTool = tap.mock('../../src/FetchTool', {
22
+ 'cross-fetch': mockFetchModule,
23
+ // Make sure FetchTool uses the same scratchFetch instance
24
+ '../../src/scratchFetch': scratchFetchModule
25
+ });
26
+
27
+ return {scratchFetchModule, FetchTool};
28
+ };
29
+
30
+ tap.test('get without metadata', async t => {
31
+ const {FetchTool} = setupModules();
32
+
33
+ const tool = new FetchTool();
34
+
35
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
36
+ const mockFetchTestData = {};
37
+ const result = await tool.get({url: '200', mockFetchTestData});
38
+
39
+ t.type(result, Uint8Array);
40
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
41
+ t.equal(mockFetchTestData.headersCount, 0);
42
+ });
43
+
44
+ tap.test('get with metadata', async t => {
45
+ const {scratchFetchModule, FetchTool} = setupModules();
46
+ const {RequestMetadata, setMetadata} = scratchFetchModule;
47
+
48
+ const tool = new FetchTool();
49
+
50
+ setMetadata(RequestMetadata.ProjectId, 1234);
51
+ setMetadata(RequestMetadata.RunId, 5678);
52
+
53
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
54
+ const mockFetchTestData = {};
55
+ const result = await tool.get({url: '200', mockFetchTestData});
56
+
57
+ t.type(result, Uint8Array);
58
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
59
+ t.equal(mockFetchTestData.headersCount, 2);
60
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), '1234');
61
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), '5678');
62
+ });
63
+
64
+ tap.test('send without metadata', async t => {
65
+ const {FetchTool} = setupModules();
66
+
67
+ const tool = new FetchTool();
68
+
69
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
70
+ const mockFetchTestData = {};
71
+ const result = await tool.send({url: '200', mockFetchTestData});
72
+
73
+ t.type(result, 'string');
74
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
75
+ t.equal(mockFetchTestData.headersCount, 0);
76
+ });
77
+
78
+ tap.test('send with metadata', async t => {
79
+ const {scratchFetchModule, FetchTool} = setupModules();
80
+ const {RequestMetadata, setMetadata} = scratchFetchModule;
81
+
82
+ const tool = new FetchTool();
83
+
84
+ setMetadata(RequestMetadata.ProjectId, 4321);
85
+ setMetadata(RequestMetadata.RunId, 8765);
86
+
87
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
88
+ const mockFetchTestData = {};
89
+ const result = await tool.send({url: '200', mockFetchTestData});
90
+
91
+ t.type(result, 'string');
92
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
93
+ t.equal(mockFetchTestData.headersCount, 2);
94
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), '4321');
95
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), '8765');
96
+ });
97
+
98
+ tap.test('selectively delete metadata', async t => {
99
+ const {scratchFetchModule, FetchTool} = setupModules();
100
+ const {RequestMetadata, setMetadata, unsetMetadata} = scratchFetchModule;
101
+
102
+ // verify that these special values are preserved and not interpreted as "delete"
103
+ setMetadata(RequestMetadata.ProjectId, null);
104
+ setMetadata(RequestMetadata.RunId, void 0); // void 0 = undefined
105
+
106
+ const tool = new FetchTool();
107
+
108
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
109
+ const mockFetchTestData = {};
110
+
111
+ const result1 = await tool.send({url: '200', mockFetchTestData});
112
+ t.type(result1, 'string');
113
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
114
+
115
+ t.equal(mockFetchTestData.headersCount, 2);
116
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), 'null'); // string "null" means it's present
117
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), 'undefined');
118
+
119
+ // remove the Project ID from metadata
120
+ unsetMetadata(RequestMetadata.ProjectId);
121
+
122
+ const result2 = await tool.send({url: '200', mockFetchTestData});
123
+ t.type(result2, 'string');
124
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
125
+
126
+ t.equal(mockFetchTestData.headersCount, 1);
127
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), null); // value `null` means it's missing
128
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), 'undefined');
129
+ });
130
+
131
+ tap.test('metadata has case-insensitive keys', async t => {
132
+ const {scratchFetchModule, FetchTool} = setupModules();
133
+ const {setMetadata} = scratchFetchModule;
134
+
135
+ setMetadata('foo', 1);
136
+ setMetadata('FOO', 2);
137
+
138
+ const tool = new FetchTool();
139
+
140
+ /** @type import('../mocks/mock-fetch.js').MockFetchTestData */
141
+ const mockFetchTestData = {};
142
+ await tool.get({url: '200', mockFetchTestData});
143
+
144
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
145
+ t.equal(mockFetchTestData.headersCount, 1);
146
+ t.equal(mockFetchTestData.headers?.get('foo'), '2');
147
+ });