diff --git a/.github/workflows/s3-bucket.yml b/.github/workflows/s3-bucket.yml new file mode 100644 index 00000000..06df7c20 --- /dev/null +++ b/.github/workflows/s3-bucket.yml @@ -0,0 +1,44 @@ +name: S3 Bucket Test + +on: + push: + workflow_dispatch: + +jobs: + test-on-os-node-matrix: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node: [18, 20, 22] + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + + name: Test S3 Bucket - Node ${{ matrix.node }} on ${{ matrix.os }} + + steps: + - name: Checkout ${{ github.ref }} + uses: actions/checkout@v4 + + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: NPM Install + run: npm install + + - name: Show Environment Info + run: | + printenv + node --version + npm --version + + - name: Run S3 Tests (against ${{ env.S3_BUCKET }} bucket) + run: | + npm run bucket ${{ env.S3_BUCKET }} + npm run test:s3 + if: ${{ env.S3_BUCKET != '' }} + diff --git a/lib/install.js b/lib/install.js index 617dd866..bdbb3b9d 100644 --- a/lib/install.js +++ b/lib/install.js @@ -233,3 +233,10 @@ function install(gyp, argv, callback) { }); } } + +// setting an environment variable: node_pre_gyp_mock_s3 to any value +// enables intercepting outgoing http requests to s3 (using nock) and +// serving them from a mocked S3 file system (using mock-aws-s3) +if (process.env.node_pre_gyp_mock_s3) { + require('./mock/http')(); +} diff --git a/lib/mock/http.js b/lib/mock/http.js new file mode 100644 index 00000000..43f9ac8d --- /dev/null +++ b/lib/mock/http.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = exports = http_mock; + +const fs = require('fs'); +const path = require('path'); +const nock = require('nock'); +const os = require('os'); + +const log = require('npmlog'); +log.disableProgress(); // disable the display of a progress bar +log.heading = 'node-pre-gyp'; // differentiate node-pre-gyp's logs from npm's + +function http_mock() { + log.warn('mocking http requests to s3'); + + const basePath = `${os.tmpdir()}/mock`; + + nock(new RegExp('([a-z0-9]+[.])*s3[.]us-east-1[.]amazonaws[.]com')) + .persist() + .get(() => true) //a function that always returns true is a catch all for nock + .reply( + (uri) => { + const bucket = 'npg-mock-bucket'; + const mockDir = uri.indexOf(bucket) === -1 ? `${basePath}/${bucket}` : basePath; + const filepath = path.join(mockDir, uri.replace(new RegExp('%2B', 'g'), '+')); + + try { + fs.accessSync(filepath, fs.constants.R_OK); + } catch (e) { + return [404, 'not found\n']; + } + + // mock s3 functions write to disk + // return what is read from it. + return [200, fs.createReadStream(filepath)]; + } + ); +} diff --git a/lib/mock/s3.js b/lib/mock/s3.js new file mode 100644 index 00000000..076b995b --- /dev/null +++ b/lib/mock/s3.js @@ -0,0 +1,42 @@ +'use strict'; + +module.exports = exports = s3_mock; + +const AWSMock = require('mock-aws-s3'); +const os = require('os'); + +const log = require('npmlog'); +log.disableProgress(); // disable the display of a progress bar +log.heading = 'node-pre-gyp'; // differentiate node-pre-gyp's logs from npm's + +function s3_mock() { + log.warn('mocking s3 operations'); + + AWSMock.config.basePath = `${os.tmpdir()}/mock`; + + const s3 = AWSMock.S3(); + + // wrapped callback maker. fs calls return code of ENOENT but AWS.S3 returns + // NotFound. + const wcb = (fn) => (err, ...args) => { + if (err && err.code === 'ENOENT') { + err.code = 'NotFound'; + } + return fn(err, ...args); + }; + + return { + listObjects(params, callback) { + return s3.listObjects(params, wcb(callback)); + }, + headObject(params, callback) { + return s3.headObject(params, wcb(callback)); + }, + deleteObject(params, callback) { + return s3.deleteObject(params, wcb(callback)); + }, + putObject(params, callback) { + return s3.putObject(params, wcb(callback)); + } + }; +} diff --git a/lib/node-pre-gyp.js b/lib/node-pre-gyp.js index dc18e749..f6c262de 100644 --- a/lib/node-pre-gyp.js +++ b/lib/node-pre-gyp.js @@ -10,18 +10,13 @@ module.exports = exports; * Module dependencies. */ -// load mocking control function for accessing s3 via https. the function is a noop always returning -// false if not mocking. -exports.mockS3Http = require('./util/s3_setup').get_mockS3Http(); -exports.mockS3Http('on'); -const mocking = exports.mockS3Http('get'); - - const fs = require('fs'); const path = require('path'); const nopt = require('nopt'); const log = require('npmlog'); -log.disableProgress(); +log.disableProgress(); // disable the display of a progress bar +log.heading = 'node-pre-gyp'; // differentiate node-pre-gyp's logs from npm's + const napi = require('./util/napi.js'); const EE = require('events').EventEmitter; @@ -43,12 +38,6 @@ const cli_commands = [ ]; const aliases = {}; -// differentiate node-pre-gyp's logs from npm's -log.heading = 'node-pre-gyp'; - -if (mocking) { - log.warn(`mocking s3 to ${process.env.node_pre_gyp_mock_s3}`); -} // this is a getter to avoid circular reference warnings with node v14. Object.defineProperty(exports, 'find', { diff --git a/lib/util/s3_setup.js b/lib/util/s3_setup.js index 52839e3b..0095cd34 100644 --- a/lib/util/s3_setup.js +++ b/lib/util/s3_setup.js @@ -3,8 +3,6 @@ module.exports = exports; const url = require('url'); -const fs = require('fs'); -const path = require('path'); module.exports.detect = function(opts) { const config = {}; @@ -60,40 +58,11 @@ module.exports.detect = function(opts) { }; module.exports.get_s3 = function(config) { - + // setting an environment variable: node_pre_gyp_mock_s3 to any value + // enables intercepting outgoing http requests to s3 (using nock) and + // serving them from a mocked S3 file system (using mock-aws-s3) if (process.env.node_pre_gyp_mock_s3) { - // here we're mocking. node_pre_gyp_mock_s3 is the scratch directory - // for the mock code. - const AWSMock = require('mock-aws-s3'); - const os = require('os'); - - AWSMock.config.basePath = `${os.tmpdir()}/mock`; - - const s3 = AWSMock.S3(); - - // wrapped callback maker. fs calls return code of ENOENT but AWS.S3 returns - // NotFound. - const wcb = (fn) => (err, ...args) => { - if (err && err.code === 'ENOENT') { - err.code = 'NotFound'; - } - return fn(err, ...args); - }; - - return { - listObjects(params, callback) { - return s3.listObjects(params, wcb(callback)); - }, - headObject(params, callback) { - return s3.headObject(params, wcb(callback)); - }, - deleteObject(params, callback) { - return s3.deleteObject(params, wcb(callback)); - }, - putObject(params, callback) { - return s3.putObject(params, wcb(callback)); - } - }; + return require('../mock/s3')(); } // if not mocking then setup real s3. @@ -117,71 +86,4 @@ module.exports.get_s3 = function(config) { return s3.putObject(params, callback); } }; - - - }; - -// -// function to get the mocking control function. if not mocking it returns a no-op. -// -// if mocking it sets up the mock http interceptors that use the mocked s3 file system -// to fulfill responses. -module.exports.get_mockS3Http = function() { - let mock_s3 = false; - if (!process.env.node_pre_gyp_mock_s3) { - return () => mock_s3; - } - - const nock = require('nock'); - // the bucket used for testing, as addressed by https. - const host = 'https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com'; - const mockDir = process.env.node_pre_gyp_mock_s3 + '/mapbox-node-pre-gyp-public-testing-bucket'; - - // function to setup interceptors. they are "turned off" by setting mock_s3 to false. - const mock_http = () => { - // eslint-disable-next-line no-unused-vars - function get(uri, requestBody) { - const filepath = path.join(mockDir, uri.replace('%2B', '+')); - - try { - fs.accessSync(filepath, fs.constants.R_OK); - } catch (e) { - return [404, 'not found\n']; - } - - // the mock s3 functions just write to disk, so just read from it. - return [200, fs.createReadStream(filepath)]; - } - - // eslint-disable-next-line no-unused-vars - return nock(host) - .persist() - .get(() => mock_s3) // mock any uri for s3 when true - .reply(get); - }; - - // setup interceptors. they check the mock_s3 flag to determine whether to intercept. - mock_http(nock, host, mockDir); - // function to turn matching all requests to s3 on/off. - const mockS3Http = (action) => { - const previous = mock_s3; - if (action === 'off') { - mock_s3 = false; - } else if (action === 'on') { - mock_s3 = true; - } else if (action !== 'get') { - throw new Error(`illegal action for setMockHttp ${action}`); - } - return previous; - }; - - // call mockS3Http with the argument - // - 'on' - turn it on - // - 'off' - turn it off (used by fetch.test.js so it doesn't interfere with redirects) - // - 'get' - return true or false for 'on' or 'off' - return mockS3Http; -}; - - - diff --git a/package.json b/package.json index af25cf10..86c958cc 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "lint": "eslint bin/node-pre-gyp lib/*js lib/util/*js test/*js scripts/*js", "fix": "npm run lint -- --fix", "update-crosswalk": "node scripts/abi_crosswalk.js", - "test": "tape test/*test.js" + "test": "tape test/*test.js", + "test:s3": "tape test/s3.test.js", + "bucket": "node scripts/set-bucket.js" } } diff --git a/scripts/set-bucket.js b/scripts/set-bucket.js new file mode 100644 index 00000000..894d77b4 --- /dev/null +++ b/scripts/set-bucket.js @@ -0,0 +1,47 @@ +'use strict'; + +// script changes the bucket name set in package.json of the test apps. + +const fs = require('fs'); +const path = require('path'); + +// http mock (lib/mock/http.js) sets 'npg-mock-bucket' as default bucket name. +// when providing no bucket name as argument, script will set +// all apps back to default mock settings. +const bucket = process.argv[2] || 'npg-mock-bucket'; + +const root = '../test'; +const rootPath = path.resolve(__dirname, root); +const dirs = fs.readdirSync(rootPath).filter((fileorDir) => fs.lstatSync(path.resolve(rootPath, fileorDir)).isDirectory()); + +dirs.forEach((dir) => { + const pkg = require(`${root}/${dir}/package.json`); // relative path + + // bucket specified as part of s3 virtual host format (auto detected by node-pre-gyp) + const keys = ['host', 'staging_host', 'production_host']; + keys.forEach((item) => { + if (pkg.binary[item]) { + + // match the bucket part of the url + const match = pkg.binary[item].match(/^https:\/\/(.+)(?:\.s3[-.].*)$/i); + if (match) { + pkg.binary[item] = pkg.binary[item].replace(match[1], bucket); + console.log(`Success: set ${dir} ${item} to ${pkg.binary[item]}`); + } + } + }); + // bucket is specified explicitly + if (pkg.binary.bucket) { + pkg.binary.bucket = bucket; + console.log(`Set ${dir} bucket to ${pkg.binary.bucket}`); + } + + // make sure bucket name is set in the package (somewhere) else this is an obvious error. + // most likely due to manual editing of the json resulting in unusable format + const str = JSON.stringify(pkg, null, 4); + if (str.indexOf(bucket) !== -1) { + fs.writeFileSync(path.join(path.resolve(rootPath, dir), 'package.json'), str + '\n'); + } else { + throw new Error(`Error: could not set ${dir}. Manually check package.json`); + } +}); diff --git a/scripts/switch-s3-hosts.js b/scripts/switch-s3-hosts.js deleted file mode 100644 index 3dd25bbf..00000000 --- a/scripts/switch-s3-hosts.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -// -// utility to switch s3 targets for local testing. if the s3 buckets are left -// pointing to the mapbox-node-pre-gyp-public-testing-bucket and you don't have -// write permissions to those buckets then the tests will fail. switching the -// target allows the tests to be run locally (even though the CI tests will fail -// if you are not a collaborator to the mapbox/node-pre-gyp repository). -// -// this replaces the mapbox-specific s3 URLs with an URL pointing to an S3 -// bucket which can be written to. each person using this will need to supply -// their own `toLocal.target` and `toMapbox.source` values that refer to their -// s3 buckets (and set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY -// appropriately). -// -// reset to the mapbox settings before committing. -// - -const fs = require('fs'); -const walk = require('action-walk'); // eslint-disable-line node/no-missing-require - -const [maj, min] = process.versions.node.split('.'); -if (`${maj}.${min}` < 10.1) { - console.error('requires node >= 10.1 for fs.promises'); - process.exit(1); -} -if (process.argv[2] !== 'toLocal' && process.argv[2] !== 'toMapbox') { - console.error('argument must be toLocal or toMapbox, not', process.argv[2]); - process.exit(1); -} - -const direction = { - toLocal: { - source: /mapbox-node-pre-gyp-public-testing-bucket/g, - target: 'bmac-pre-gyp-test' - }, - toMapbox: { - source: /bmac-pre-gyp-test/g, - target: 'mapbox-node-pre-gyp-public-testing-bucket' - } -}; - -const repl = direction[process.argv[2]]; - -console.log('replacing:'); -console.log(' ', repl.source); -console.log('with:'); -console.log(' ', repl.target); - - -function dirAction(path) { - if (path.startsWith('./node_modules/')) { - return 'skip'; - } -} - -function fileAction(path) { - if (path.endsWith('/package.json') || path.endsWith('/fetch.test.js') || path.endsWith('/lib/util/s3_setup.js')) { - const file = fs.readFileSync(path, 'utf8'); - const changed = file.replace(repl.source, repl.target); - if (file !== changed) { - console.log('replacing in:', path); - // eslint-disable-next-line node/no-unsupported-features/node-builtins - return fs.promises.writeFile(path, changed); - } else { - console.log('target not found in:', path); - } - } -} - -const options = { - dirAction, - fileAction -}; - -walk('.', options); diff --git a/scripts/test-node-webkit.sh b/scripts/test-node-webkit.sh index ac81ff79..8bf555bd 100755 --- a/scripts/test-node-webkit.sh +++ b/scripts/test-node-webkit.sh @@ -1,5 +1,7 @@ #!/bin/bash +nw_version=${1:-"0.50.2"} + set -eu set -o pipefail @@ -9,7 +11,7 @@ export PATH=`pwd`/bin:$PATH BASE=$(pwd) -export NODE_WEBKIT_VERSION="0.50.2" +export NODE_WEBKIT_VERSION="${nw_version}" export NW_INSTALL_URL="https://dl.nwjs.io" if [[ `uname -s` == 'Darwin' ]]; then diff --git a/test/app1.1/.gitignore b/test/app1.1/.gitignore new file mode 100644 index 00000000..f6a05031 --- /dev/null +++ b/test/app1.1/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +build/ +lib/binding/ +node_modules +npm-debug.log \ No newline at end of file diff --git a/test/app1.1/README.md b/test/app1.1/README.md new file mode 100644 index 00000000..1210b152 --- /dev/null +++ b/test/app1.1/README.md @@ -0,0 +1,4 @@ +# Test app + +Demonstrates a simple configuration that uses node-pre-gyp. +Identical to app1 but using production and staging binary host option. diff --git a/test/app1.1/app1.1.cc b/test/app1.1/app1.1.cc new file mode 100644 index 00000000..4c8a8d87 --- /dev/null +++ b/test/app1.1/app1.1.cc @@ -0,0 +1,14 @@ +#include + +Napi::Value get_hello(Napi::CallbackInfo const& info) { + Napi::Env env = info.Env(); + Napi::EscapableHandleScope scope(env); + return scope.Escape(Napi::String::New(env, "hello")); +} + +Napi::Object start(Napi::Env env, Napi::Object exports) { + exports.Set("hello", Napi::Function::New(env, get_hello)); + return exports; +} + +NODE_API_MODULE(app1, start) diff --git a/test/app1.1/binding.gyp b/test/app1.1/binding.gyp new file mode 100644 index 00000000..05ef4f1c --- /dev/null +++ b/test/app1.1/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [ + { + "target_name": "<(module_name)", + "sources": [ "<(module_name).cc" ], + 'product_dir': '<(module_path)', + 'include_dirs': ["../../node_modules/node-addon-api/"], + 'cflags!': [ '-fno-exceptions' ], + 'cflags_cc!': [ '-fno-exceptions' ], + "xcode_settings": { + 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', + "CLANG_CXX_LIBRARY": "libc++" + }, + 'msvs_settings': { + 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, + } + } + ] +} diff --git a/test/app1.1/index.js b/test/app1.1/index.js new file mode 100644 index 00000000..084fc583 --- /dev/null +++ b/test/app1.1/index.js @@ -0,0 +1,6 @@ +var binary = require('node-pre-gyp'); +var path = require('path') +var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json'))); +var binding = require(binding_path); + +require('assert').equal(binding.hello(),"hello"); \ No newline at end of file diff --git a/test/app1.1/package.json b/test/app1.1/package.json new file mode 100644 index 00000000..ddc9233b --- /dev/null +++ b/test/app1.1/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-pre-gyp-test-app1.1", + "author": "Dane Springmeyer ", + "description": "node-pre-gyp test", + "repository": { + "type": "git", + "url": "git://github.com/mapbox/node-pre-gyp.git" + }, + "license": "BSD-3-Clause", + "version": "0.1.0", + "main": "./index.js", + "binary": { + "module_name": "app1.1", + "module_path": "./lib/binding/", + "staging_host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com", + "production_host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com", + "remote_path": "./node-pre-gyp/{name}/v{version}/{configuration}/{toolset}/", + "package_name": "{node_abi}-{platform}-{arch}.tar.gz" + }, + "scripts": { + "install": "node-pre-gyp install --fallback-to-build", + "test": "node index.js" + } +} diff --git a/test/app1.2/.gitignore b/test/app1.2/.gitignore new file mode 100644 index 00000000..f6a05031 --- /dev/null +++ b/test/app1.2/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +build/ +lib/binding/ +node_modules +npm-debug.log \ No newline at end of file diff --git a/test/app1.2/README.md b/test/app1.2/README.md new file mode 100644 index 00000000..5cbf7616 --- /dev/null +++ b/test/app1.2/README.md @@ -0,0 +1,4 @@ +# Test app + +Demonstrates a simple configuration that uses node-pre-gyp. +Identical to app1 but using explicit host, region, bucket options. diff --git a/test/app1.2/app1.2.cc b/test/app1.2/app1.2.cc new file mode 100644 index 00000000..4c8a8d87 --- /dev/null +++ b/test/app1.2/app1.2.cc @@ -0,0 +1,14 @@ +#include + +Napi::Value get_hello(Napi::CallbackInfo const& info) { + Napi::Env env = info.Env(); + Napi::EscapableHandleScope scope(env); + return scope.Escape(Napi::String::New(env, "hello")); +} + +Napi::Object start(Napi::Env env, Napi::Object exports) { + exports.Set("hello", Napi::Function::New(env, get_hello)); + return exports; +} + +NODE_API_MODULE(app1, start) diff --git a/test/app1.2/binding.gyp b/test/app1.2/binding.gyp new file mode 100644 index 00000000..05ef4f1c --- /dev/null +++ b/test/app1.2/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [ + { + "target_name": "<(module_name)", + "sources": [ "<(module_name).cc" ], + 'product_dir': '<(module_path)', + 'include_dirs': ["../../node_modules/node-addon-api/"], + 'cflags!': [ '-fno-exceptions' ], + 'cflags_cc!': [ '-fno-exceptions' ], + "xcode_settings": { + 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', + "CLANG_CXX_LIBRARY": "libc++" + }, + 'msvs_settings': { + 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, + } + } + ] +} diff --git a/test/app1.2/index.js b/test/app1.2/index.js new file mode 100644 index 00000000..084fc583 --- /dev/null +++ b/test/app1.2/index.js @@ -0,0 +1,6 @@ +var binary = require('node-pre-gyp'); +var path = require('path') +var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json'))); +var binding = require(binding_path); + +require('assert').equal(binding.hello(),"hello"); \ No newline at end of file diff --git a/test/app1.2/package.json b/test/app1.2/package.json new file mode 100644 index 00000000..eac6c266 --- /dev/null +++ b/test/app1.2/package.json @@ -0,0 +1,26 @@ +{ + "name": "node-pre-gyp-test-app1.2", + "author": "Dane Springmeyer ", + "description": "node-pre-gyp test", + "repository": { + "type": "git", + "url": "git://github.com/mapbox/node-pre-gyp.git" + }, + "license": "BSD-3-Clause", + "version": "0.1.0", + "main": "./index.js", + "binary": { + "module_name": "app1.2", + "module_path": "./lib/binding/", + "host": "https://s3.us-east-1.amazonaws.com", + "bucket": "npg-mock-bucket", + "region": "us-east-1", + "s3ForcePathStyle": true, + "remote_path": "./node-pre-gyp/{name}/v{version}/{configuration}/{toolset}/", + "package_name": "{node_abi}-{platform}-{arch}.tar.gz" + }, + "scripts": { + "install": "node-pre-gyp install --fallback-to-build", + "test": "node index.js" + } +} diff --git a/test/app1/package.json b/test/app1/package.json index 3cc13212..c1b5bf79 100644 --- a/test/app1/package.json +++ b/test/app1/package.json @@ -12,7 +12,7 @@ "binary": { "module_name": "app1", "module_path": "./lib/binding/", - "host": "https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com", + "host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com", "remote_path": "./node-pre-gyp/{name}/v{version}/{configuration}/{toolset}/", "package_name": "{node_abi}-{platform}-{arch}.tar.gz" }, diff --git a/test/app2/package.json b/test/app2/package.json index d86e49a6..edfc6dbc 100644 --- a/test/app2/package.json +++ b/test/app2/package.json @@ -14,7 +14,7 @@ "module_path": "./lib/binding/{configuration}/{name}", "remote_path": "./node-pre-gyp/{name}/v{version}/{configuration}/{version}/{toolset}/", "package_name": "{module_name}-v{major}.{minor}.{patch}-{prerelease}+{build}-{node_abi}-{platform}-{arch}.tar.gz", - "host": "https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com" + "host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com" }, "scripts": { "install": "node-pre-gyp install --fallback-to-build", diff --git a/test/app3/package.json b/test/app3/package.json index faf15dae..c0d1c4a0 100644 --- a/test/app3/package.json +++ b/test/app3/package.json @@ -14,7 +14,7 @@ "module_path": "./lib/binding/{node_abi}-{platform}-{arch}", "remote_path": "./node-pre-gyp/{module_name}/v{version}", "package_name": "{node_abi}-{platform}-{arch}.tar.gz", - "host": "https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com" + "host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com" }, "scripts": { "install": "node-pre-gyp install --fallback-to-build", diff --git a/test/app4/package.json b/test/app4/package.json index 388fa4e8..998eb423 100644 --- a/test/app4/package.json +++ b/test/app4/package.json @@ -14,7 +14,7 @@ "module_path": "./lib/binding/{node_abi}-{platform}-{arch}", "remote_path": "./node-pre-gyp/{module_name}/v{version}", "package_name": "{node_abi}-{platform}-{arch}.tar.gz", - "host": "https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com" + "host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com" }, "scripts": { "install": "node-pre-gyp install --fallback-to-build", diff --git a/test/app7/package.json b/test/app7/package.json index 27e30b31..ba8e7b00 100644 --- a/test/app7/package.json +++ b/test/app7/package.json @@ -14,7 +14,7 @@ "module_path": "./lib/binding/napi-v{napi_build_version}", "remote_path": "./node-pre-gyp/{module_name}/v{version}/{configuration}/", "package_name": "{module_name}-v{version}-{platform}-{arch}-napi-v{napi_build_version}-node-{node_abi}.tar.gz", - "host": "https://mapbox-node-pre-gyp-public-testing-bucket.s3.us-east-1.amazonaws.com", + "host": "https://npg-mock-bucket.s3.us-east-1.amazonaws.com", "napi_versions": [ 1, 2 diff --git a/test/s3.test.js b/test/s3.test.js new file mode 100644 index 00000000..52123c50 --- /dev/null +++ b/test/s3.test.js @@ -0,0 +1,229 @@ +'use strict'; + +const test = require('tape'); +const run = require('./run.util.js'); +const existsSync = require('fs').existsSync || require('path').existsSync; +const fs = require('fs'); +const rm = require('rimraf'); +const path = require('path'); +const napi = require('../lib/util/napi.js'); +const versioning = require('../lib/util/versioning.js'); + +const localVer = [versioning.get_runtime_abi('node'), process.platform, process.arch].join('-'); +const SOEXT = { 'darwin': 'dylib', 'linux': 'so', 'win32': 'dll' }[process.platform]; + +// The list of different sample apps that we use to test +// apps with . in name are variation of app with different binary hosting setting +const apps = [ + { + 'name': 'app1', + 'args': '', + 'files': { + 'base': ['binding/app1.node'] + } + }, + { + 'name': 'app1.1', + 'args': '', + 'files': { + 'base': ['binding/app1.1.node'] + } + }, + { + 'name': 'app1.2', + 'args': '', + 'files': { + 'base': ['binding/app1.2.node'] + } + }, + { + 'name': 'app2', + 'args': '--custom_include_path=../include --debug', + 'files': { + 'base': ['node-pre-gyp-test-app2/app2.node'] + } + }, + { + 'name': 'app2', + 'args': '--custom_include_path=../include --toolset=cpp11', + 'files': { + 'base': ['node-pre-gyp-test-app2/app2.node'] + } + }, + { + 'name': 'app3', + 'args': '', + 'files': { + 'base': [[localVer, 'app3.node'].join('/')] + } + }, + { + 'name': 'app4', + 'args': '', + 'files': { + 'base': [[localVer, 'app4.node'].join('/'), [localVer, 'mylib.' + SOEXT].join('/')] + } + }, + { + 'name': 'app7', + 'args': '' + } +]; + + +// https://stackoverflow.com/questions/38599457/how-to-write-a-custom-assertion-for-testing-node-or-javascript-with-tape-or-che +test.Test.prototype.stringContains = function(actual, contents, message) { + this._assert(actual.indexOf(contents) > -1, { + message: message || 'should contain ' + contents, + operator: 'stringContains', + actual: actual, + expected: contents + }); +}; + +// Tests run for all apps + +apps.forEach((app) => { + + if (app.name === 'app7' && !napi.get_napi_version()) return; + + // clear out entire binding directory + // to ensure no stale builds. This is needed + // because "node-pre-gyp clean" only removes + // the current target and not alternative builds + test('cleanup of app', (t) => { + const binding_directory = path.join(__dirname, app.name, 'lib/binding'); + if (fs.existsSync(binding_directory)) { + rm.sync(binding_directory); + } + t.end(); + }); + + test(app.name + ' build ' + app.args, (t) => { + run('node-pre-gyp', 'rebuild', '--fallback-to-build', app, {}, (err, stdout, stderr) => { + t.ifError(err); + if (err) { + console.log(stdout); + console.log(stderr); + } + t.end(); + }); + }); + + test(app.name + ' package ' + app.args, (t) => { + run('node-pre-gyp', 'package', '', app, {}, (err) => { + t.ifError(err); + // Make sure a tarball was created + run('node-pre-gyp', 'reveal', 'staged_tarball --silent', app, {}, (err2, stdout) => { + t.ifError(err2); + let staged_tarball = stdout.trim(); + if (staged_tarball.indexOf('\n') !== -1) { // take just the first line + staged_tarball = staged_tarball.substr(0, staged_tarball.indexOf('\n')); + } + const tarball_path = path.join(__dirname, app.name, staged_tarball); + t.ok(existsSync(tarball_path), 'staged tarball is a valid file'); + if (!app.files) { + return t.end(); + } + t.end(); + }); + }); + }); + + test(app.name + ' package is valid ' + app.args, (t) => { + run('node-pre-gyp', 'testpackage', '', app, {}, (err) => { + t.ifError(err); + t.end(); + }); + }); + + if (process.env.AWS_ACCESS_KEY_ID || process.env.node_pre_gyp_accessKeyId || process.env.node_pre_gyp_mock_s3) { + + test(app.name + ' publishes ' + app.args, (t) => { + run('node-pre-gyp', 'unpublish publish', '', app, {}, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + test(app.name + ' info shows it ' + app.args, (t) => { + run('node-pre-gyp', 'reveal', 'package_name', app, {}, (err, stdout) => { + t.ifError(err); + let package_name = stdout.trim(); + if (package_name.indexOf('\n') !== -1) { // take just the first line + package_name = package_name.substr(0, package_name.indexOf('\n')); + } + run('node-pre-gyp', 'info', '', app, {}, (err2, stdout2) => { + t.ifError(err2); + t.stringContains(stdout2, package_name); + t.end(); + }); + }); + }); + + test(app.name + ' can be uninstalled ' + app.args, (t) => { + run('node-pre-gyp', 'clean', '', app, {}, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + test(app.name + ' can be installed via remote ' + app.args, (t) => { + const opts = { + cwd: path.join(__dirname, app.name), + npg_debug: false + }; + run('npm', 'install', '--fallback-to-build=false', app, opts, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + test(app.name + ' can be reinstalled via remote ' + app.args, (t) => { + const opts = { + cwd: path.join(__dirname, app.name), + npg_debug: false + }; + run('npm', 'install', '--update-binary --fallback-to-build=false', app, opts, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + test(app.name + ' via remote passes tests ' + app.args, (t) => { + const opts = { + cwd: path.join(__dirname, app.name), + npg_debug: false + }; + run('npm', 'install', '', app, opts, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + test(app.name + ' unpublishes ' + app.args, (t) => { + run('node-pre-gyp', 'unpublish', '', app, {}, (err, stdout) => { + t.ifError(err); + t.notEqual(stdout, ''); + t.end(); + }); + }); + + } else { + test.skip(app.name + ' publishes ' + app.args, () => {}); + } + + // note: the above test will result in a non-runnable binary, so the below test must succeed otherwise all following tests will fail + + test(app.name + ' builds with custom --target ' + app.args, (t) => { + run('node-pre-gyp', 'rebuild', '--loglevel=error --fallback-to-build --target=' + process.versions.node, app, {}, (err) => { + t.ifError(err); + t.end(); + }); + }); +});