Skip to content

Commit c0258ab

Browse files
authored
Create GitHub Actions Test Setup (#652)
* Refactored mock setup. - mocking setup can be more easily used by automated tests and/or manually by developer. - mock modules moved to lib/mock. - mock modules are self contained and have a signature similar to that of other modules in the package. - simplified http mock to remove no longer needed interface. - modified http mock to support path style bucket access. - set http mock to work with default npg-mock-bucket bucket. * Refactored test setup. - separated s3 tests from build tests. - refactored fetch test and proxy-bcrypt test to work with refactored mock setup. * Refactored test apps and increased test coverage - set hosts of test apps to point to mock bucket. - added app1.1 - identical to app1 but using production and staging binary host option. - added app1.2 - identical to app1 but using explicit host, region, bucket options. * Added service script to switch test apps bucket from mock to S3 and vis versa. Removed previous one. Fix CodeQL errors. * Fix CodeQL and rebase errors. * Added service script to switch test apps bucket from mock to S3 and vis versa. Removed previous one. Fix CodeQL errors. * Created GitHub Actions Test Setup - Added a GitHub Actions workflow that runs whenever there is a push to the repo. - Workflow includes two jobs: - A matrix job of node versions (10, 12, 14, 16, 18) and operating systems (Linux (ubuntu), Mac and Windows (2019 Enterprise)) that runs all tests against mock and then runs s3 tests against a bucket (located at us-east-1-bucket) specified as a repo secret. - A matrix job of and NW.js versions (0.64.0, 0.50.2) and node versions (10, 12, ,14, 16) that runs the NW.js test script. * Modified existing configurations to work with GitHub Actions Test setup. - Modified `scripts/test-node-webkit.sh` so that it can now accept an NW.js version as input. This allows running the script in a GitHub Actions matrix. - Modified `test/run.util.js` so that it does not set `--msvs_version=2015` when running in GitHub Actions. This is required because current GitHub Actions runner do not support VS Studio 2015. - Added npm script command `test:s3` to `package.json` that runs only the s3 tests. This is required because invoking `npx tape test/s3.test.js` on windows does not work as expected. - Modified `test/proxy-bcrypt.test.js`. Removed uneeded CI conditionals and modified download directory setup/cleanup. Latter was required due to concurrency issues with running tests on Mac, resulting in uncatchable errors during directory removal originating from `rimraf`. * Updated github actions to only test node 18, 20, 22. * Trying windows-latest * Removed nw tests from GitHub push actions as they are not working right now. * Changed push to bucket only tests. * Changed test name * Changed test name
1 parent 986a12f commit c0258ab

28 files changed

+568
-196
lines changed

.github/workflows/s3-bucket.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: S3 Bucket Test
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
7+
jobs:
8+
test-on-os-node-matrix:
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
matrix:
12+
os: [ubuntu-latest, macos-latest, windows-latest]
13+
node: [18, 20, 22]
14+
env:
15+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
16+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
17+
S3_BUCKET: ${{ secrets.S3_BUCKET }}
18+
19+
name: Test S3 Bucket - Node ${{ matrix.node }} on ${{ matrix.os }}
20+
21+
steps:
22+
- name: Checkout ${{ github.ref }}
23+
uses: actions/checkout@v4
24+
25+
- name: Setup node ${{ matrix.node }}
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: ${{ matrix.node }}
29+
30+
- name: NPM Install
31+
run: npm install
32+
33+
- name: Show Environment Info
34+
run: |
35+
printenv
36+
node --version
37+
npm --version
38+
39+
- name: Run S3 Tests (against ${{ env.S3_BUCKET }} bucket)
40+
run: |
41+
npm run bucket ${{ env.S3_BUCKET }}
42+
npm run test:s3
43+
if: ${{ env.S3_BUCKET != '' }}
44+

lib/install.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,10 @@ function install(gyp, argv, callback) {
232232
});
233233
}
234234
}
235+
236+
// setting an environment variable: node_pre_gyp_mock_s3 to any value
237+
// enables intercepting outgoing http requests to s3 (using nock) and
238+
// serving them from a mocked S3 file system (using mock-aws-s3)
239+
if (process.env.node_pre_gyp_mock_s3) {
240+
require('./mock/http')();
241+
}

lib/mock/http.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
module.exports = exports = http_mock;
4+
5+
const fs = require('fs');
6+
const path = require('path');
7+
const nock = require('nock');
8+
const os = require('os');
9+
10+
const log = require('npmlog');
11+
log.disableProgress(); // disable the display of a progress bar
12+
log.heading = 'node-pre-gyp'; // differentiate node-pre-gyp's logs from npm's
13+
14+
function http_mock() {
15+
log.warn('mocking http requests to s3');
16+
17+
const basePath = `${os.tmpdir()}/mock`;
18+
19+
nock(new RegExp('([a-z0-9]+[.])*s3[.]us-east-1[.]amazonaws[.]com'))
20+
.get(() => true) //a function that always returns true is a catch all for nock
21+
.reply(
22+
(uri) => {
23+
const bucket = 'npg-mock-bucket';
24+
const mockDir = uri.indexOf(bucket) === -1 ? `${basePath}/${bucket}` : basePath;
25+
const filepath = path.join(mockDir, uri.replace(new RegExp('%2B', 'g'), '+'));
26+
27+
try {
28+
fs.accessSync(filepath, fs.constants.R_OK);
29+
} catch (e) {
30+
return [404, 'not found\n'];
31+
}
32+
33+
// mock s3 functions write to disk
34+
// return what is read from it.
35+
return [200, fs.createReadStream(filepath)];
36+
}
37+
);
38+
}

lib/mock/s3.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
module.exports = exports = s3_mock;
4+
5+
const AWSMock = require('mock-aws-s3');
6+
const os = require('os');
7+
8+
const log = require('npmlog');
9+
log.disableProgress(); // disable the display of a progress bar
10+
log.heading = 'node-pre-gyp'; // differentiate node-pre-gyp's logs from npm's
11+
12+
function s3_mock() {
13+
log.warn('mocking s3 operations');
14+
15+
AWSMock.config.basePath = `${os.tmpdir()}/mock`;
16+
17+
const s3 = AWSMock.S3();
18+
19+
// wrapped callback maker. fs calls return code of ENOENT but AWS.S3 returns
20+
// NotFound.
21+
const wcb = (fn) => (err, ...args) => {
22+
if (err && err.code === 'ENOENT') {
23+
err.code = 'NotFound';
24+
}
25+
return fn(err, ...args);
26+
};
27+
28+
return {
29+
listObjects(params, callback) {
30+
return s3.listObjects(params, wcb(callback));
31+
},
32+
headObject(params, callback) {
33+
return s3.headObject(params, wcb(callback));
34+
},
35+
deleteObject(params, callback) {
36+
return s3.deleteObject(params, wcb(callback));
37+
},
38+
putObject(params, callback) {
39+
return s3.putObject(params, wcb(callback));
40+
}
41+
};
42+
}

lib/node-pre-gyp.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,6 @@ module.exports = exports;
1010
* Module dependencies.
1111
*/
1212

13-
// load mocking control function for accessing s3 via https. the function is a noop always returning
14-
// false if not mocking.
15-
exports.mockS3Http = require('./util/s3_setup').get_mockS3Http();
16-
exports.mockS3Http('on');
17-
const mocking = exports.mockS3Http('get');
18-
19-
2013
const fs = require('fs');
2114
const path = require('path');
2215
const nopt = require('nopt');
@@ -42,10 +35,6 @@ const cli_commands = [
4235
];
4336
const aliases = {};
4437

45-
if (mocking) {
46-
log.warn(`mocking s3 to ${process.env.node_pre_gyp_mock_s3}`);
47-
}
48-
4938
// this is a getter to avoid circular reference warnings with node v14.
5039
Object.defineProperty(exports, 'find', {
5140
get: function() {

lib/util/s3_setup.js

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
module.exports = exports;
44

55
const url = require('url');
6-
const fs = require('fs');
7-
const path = require('path');
86

97
module.exports.detect = function(opts) {
108
const config = {};
@@ -60,40 +58,11 @@ module.exports.detect = function(opts) {
6058
};
6159

6260
module.exports.get_s3 = function(config) {
63-
61+
// setting an environment variable: node_pre_gyp_mock_s3 to any value
62+
// enables intercepting outgoing http requests to s3 (using nock) and
63+
// serving them from a mocked S3 file system (using mock-aws-s3)
6464
if (process.env.node_pre_gyp_mock_s3) {
65-
// here we're mocking. node_pre_gyp_mock_s3 is the scratch directory
66-
// for the mock code.
67-
const AWSMock = require('mock-aws-s3');
68-
const os = require('os');
69-
70-
AWSMock.config.basePath = `${os.tmpdir()}/mock`;
71-
72-
const s3 = AWSMock.S3();
73-
74-
// wrapped callback maker. fs calls return code of ENOENT but AWS.S3 returns
75-
// NotFound.
76-
const wcb = (fn) => (err, ...args) => {
77-
if (err && err.code === 'ENOENT') {
78-
err.code = 'NotFound';
79-
}
80-
return fn(err, ...args);
81-
};
82-
83-
return {
84-
listObjects(params, callback) {
85-
return s3.listObjects(params, wcb(callback));
86-
},
87-
headObject(params, callback) {
88-
return s3.headObject(params, wcb(callback));
89-
},
90-
deleteObject(params, callback) {
91-
return s3.deleteObject(params, wcb(callback));
92-
},
93-
putObject(params, callback) {
94-
return s3.putObject(params, wcb(callback));
95-
}
96-
};
65+
return require('../mock/s3')();
9766
}
9867

9968
// if not mocking then setup real s3.
@@ -117,71 +86,4 @@ module.exports.get_s3 = function(config) {
11786
return s3.putObject(params, callback);
11887
}
11988
};
120-
121-
122-
12389
};
124-
125-
//
126-
// function to get the mocking control function. if not mocking it returns a no-op.
127-
//
128-
// if mocking it sets up the mock http interceptors that use the mocked s3 file system
129-
// to fulfill responses.
130-
module.exports.get_mockS3Http = function() {
131-
let mock_s3 = false;
132-
if (!process.env.node_pre_gyp_mock_s3) {
133-
return () => mock_s3;
134-
}
135-
136-
const nock = require('nock');
137-
// the bucket used for testing, as addressed by https.
138-
const host = 'https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com';
139-
const mockDir = process.env.node_pre_gyp_mock_s3 + '/mapbox-node-pre-gyp-public-testing-bucket';
140-
141-
// function to setup interceptors. they are "turned off" by setting mock_s3 to false.
142-
const mock_http = () => {
143-
// eslint-disable-next-line no-unused-vars
144-
function get(uri, requestBody) {
145-
const filepath = path.join(mockDir, uri.replace('%2B', '+'));
146-
147-
try {
148-
fs.accessSync(filepath, fs.constants.R_OK);
149-
} catch (e) {
150-
return [404, 'not found\n'];
151-
}
152-
153-
// the mock s3 functions just write to disk, so just read from it.
154-
return [200, fs.createReadStream(filepath)];
155-
}
156-
157-
// eslint-disable-next-line no-unused-vars
158-
return nock(host)
159-
.persist()
160-
.get(() => mock_s3) // mock any uri for s3 when true
161-
.reply(get);
162-
};
163-
164-
// setup interceptors. they check the mock_s3 flag to determine whether to intercept.
165-
mock_http(nock, host, mockDir);
166-
// function to turn matching all requests to s3 on/off.
167-
const mockS3Http = (action) => {
168-
const previous = mock_s3;
169-
if (action === 'off') {
170-
mock_s3 = false;
171-
} else if (action === 'on') {
172-
mock_s3 = true;
173-
} else if (action !== 'get') {
174-
throw new Error(`illegal action for setMockHttp ${action}`);
175-
}
176-
return previous;
177-
};
178-
179-
// call mockS3Http with the argument
180-
// - 'on' - turn it on
181-
// - 'off' - turn it off (used by fetch.test.js so it doesn't interfere with redirects)
182-
// - 'get' - return true or false for 'on' or 'off'
183-
return mockS3Http;
184-
};
185-
186-
187-

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"lint": "eslint bin/node-pre-gyp lib/*js lib/util/*js test/*js scripts/*js",
5959
"fix": "npm run lint -- --fix",
6060
"update-crosswalk": "node scripts/abi_crosswalk.js",
61-
"test": "tape test/*test.js"
61+
"test": "tape test/*test.js",
62+
"test:s3": "tape test/s3.test.js",
63+
"bucket": "node scripts/set-bucket.js"
6264
}
6365
}

scripts/set-bucket.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
// script changes the bucket name set in package.json of the test apps.
4+
5+
const fs = require('fs');
6+
const path = require('path');
7+
8+
// http mock (lib/mock/http.js) sets 'npg-mock-bucket' as default bucket name.
9+
// when providing no bucket name as argument, script will set
10+
// all apps back to default mock settings.
11+
const bucket = process.argv[2] || 'npg-mock-bucket';
12+
13+
const root = '../test';
14+
const rootPath = path.resolve(__dirname, root);
15+
const dirs = fs.readdirSync(rootPath).filter((fileorDir) => fs.lstatSync(path.resolve(rootPath, fileorDir)).isDirectory());
16+
17+
dirs.forEach((dir) => {
18+
const pkg = require(`${root}/${dir}/package.json`); // relative path
19+
20+
// bucket specified as part of s3 virtual host format (auto detected by node-pre-gyp)
21+
const keys = ['host', 'staging_host', 'production_host'];
22+
keys.forEach((item) => {
23+
if (pkg.binary[item]) {
24+
25+
// match the bucket part of the url
26+
const match = pkg.binary[item].match(/^https:\/\/(.+)(?:\.s3[-.].*)$/i);
27+
if (match) {
28+
pkg.binary[item] = pkg.binary[item].replace(match[1], bucket);
29+
console.log(`Success: set ${dir} ${item} to ${pkg.binary[item]}`);
30+
}
31+
}
32+
});
33+
// bucket is specified explicitly
34+
if (pkg.binary.bucket) {
35+
pkg.binary.bucket = bucket;
36+
console.log(`Set ${dir} bucket to ${pkg.binary.bucket}`);
37+
}
38+
39+
// make sure bucket name is set in the package (somewhere) else this is an obvious error.
40+
// most likely due to manual editing of the json resulting in unusable format
41+
const str = JSON.stringify(pkg, null, 4);
42+
if (str.indexOf(bucket) !== -1) {
43+
fs.writeFileSync(path.join(path.resolve(rootPath, dir), 'package.json'), str + '\n');
44+
} else {
45+
throw new Error(`Error: could not set ${dir}. Manually check package.json`);
46+
}
47+
});

0 commit comments

Comments
 (0)