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/README.md b/README.md index 533256bc..35e1192f 100644 --- a/README.md +++ b/README.md @@ -100,25 +100,29 @@ This is a guide to configuring your module to use node-pre-gyp. - Add `@mapbox/node-pre-gyp` to `dependencies` - Add `aws-sdk` as a `devDependency` - Add a custom `install` script - - Declare a `binary` object + - Declare a `binary` and spcify `host` object This looks like: ```js - "dependencies" : { - "@mapbox/node-pre-gyp": "1.x" - }, - "devDependencies": { - "aws-sdk": "2.x" - } - "scripts": { - "install": "node-pre-gyp install --fallback-to-build" - }, - "binary": { - "module_name": "your_module", - "module_path": "./lib/binding/", - "host": "https://your_module.s3-us-west-1.amazonaws.com" +{ + "dependencies":{ + "@mapbox/node-pre-gyp":"1.x" + }, + "devDependencies":{ + "aws-sdk":"2.x" + }, + "scripts":{ + "install":"node-pre-gyp install --fallback-to-build" + }, + "binary":{ + "module_name":"your_module", + "module_path":"./lib/binding/", + "host":{ + "endpoint":"https://your_module.s3-us-west-1.amazonaws.com" } + } +} ``` For a full example see [node-addon-examples's package.json](https://github.com/springmeyer/node-addon-example/blob/master/package.json). @@ -148,9 +152,9 @@ The location your native module is placed after a build. This should be an empty Note: This property supports variables based on [Versioning](#versioning). -###### host +###### host (and host.endpoint) -A url to the remote location where you've published tarball binaries (must be `https` not `http`). +An object with atleast a single key `endpoint` defining the remote location where you've published tarball binaries (must be `https` not `http`). It is highly recommended that you use Amazon S3. The reasons are: @@ -162,13 +166,21 @@ Why then not require S3? Because while some applications using node-pre-gyp need It should also be mentioned that there is an optional and entirely separate npm module called [node-pre-gyp-github](https://github.com/bchr02/node-pre-gyp-github) which is intended to complement node-pre-gyp and be installed along with it. It provides the ability to store and publish your binaries within your repositories GitHub Releases if you would rather not use S3 directly. Installation and usage instructions can be found [here](https://github.com/bchr02/node-pre-gyp-github), but the basic premise is that instead of using the ```node-pre-gyp publish``` command you would use ```node-pre-gyp-github publish```. -##### The `binary` object other optional S3 properties +This looks like: -If you are not using a standard s3 path like `bucket_name.s3(.-)region.amazonaws.com`, you might get an error on `publish` because node-pre-gyp extracts the region and bucket from the `host` url. For example, you may have an on-premises s3-compatible storage server, or may have configured a specific dns redirecting to an s3 endpoint. In these cases, you can explicitly set the `region` and `bucket` properties to tell node-pre-gyp to use these values instead of guessing from the `host` property. The following values can be used in the `binary` section: +```js +{ + "binary": { + "host": { + "endpoint": "https://some-bucket.s3.us-east-1.amazonaws.com", + } + } +} +``` -###### host +##### The `host` object other optional S3 properties -The url to the remote server root location (must be `https` not `http`). +If you are not using a standard s3 path like `bucket_name.s3(.-)region.amazonaws.com`, you might get an error on `publish` because node-pre-gyp extracts the region and bucket from the `host` url. For example, you may have an on-premises s3-compatible storage server, or may have configured a specific dns redirecting to an s3 endpoint. In these cases, you can explicitly set the `region` and `bucket` properties to tell node-pre-gyp to use these values instead of guessing from the `host` property. The following values can be used in the `binary` section: ###### bucket @@ -182,6 +194,21 @@ Your S3 server region. Set `s3ForcePathStyle` to true if the endpoint url should not be prefixed with the bucket name. If false (default), the server endpoint would be constructed as `bucket_name.your_server.com`. +For example using an alternate S3 compatible host: + +```js +{ + "binary": { + "host": { + "endpoint": "https://play.min.io", + "bucket": "node-pre-gyp-production", + "region": "us-east-1", + "s3ForcePathStyle": true + } + } +} +``` + ##### The `binary` object has optional properties ###### remote_path @@ -309,28 +336,38 @@ If a a binary was not available for a given platform and `--fallback-to-build` w #### 9) One more option -It may be that you want to work with two s3 buckets, one for staging and one for production; this -arrangement makes it less likely to accidentally overwrite a production binary. It also allows the production -environment to have more restrictive permissions than staging while still enabling publishing when -developing and testing. +It may be that you want to work with multiple s3 buckets, one for development, on for staging and one for production; such arrangement makes it less likely to accidentally overwrite a production binary. It also allows the production environment to have more restrictive permissions than development or staging while still enabling publishing when developing and testing. -The binary.host property can be set at execution time. In order to do so all of the following conditions -must be true. -- binary.host is falsey or not present -- binary.staging_host is not empty -- binary.production_host is not empty +To use that option set `staging_host` and/or `development_host` using settings similar to those used for `host`. -If any of these checks fail then the operation will not perform execution time determination of the s3 target. +``` +{ + "binary": { + "host": { + "endpoint": "https://dns.pointed.example.com", + "bucket": "obscured-production-bucket", + "region": "us-east-1", + "s3ForcePathStyle": true + } + "staging_host": { + "endpoint": "https://my-staging-bucket.s3.us-east-1.amazonaws.com", + }, + "development_host": { + "endpoint": "https://play.min.io", + "bucket": "node-pre-gyp-development", + "region": "us-east-1", + "s3ForcePathStyle": true + } + } +} +``` -If the command being executed is either "publish" or "unpublish" then the default is set to `binary.staging_host`. In all other cases -the default is `binary.production_host`. +Once a development and/or staging host is defined, if the command being executed is either "publish" or "unpublish" then it will default to the lower of the alternate hosts (development and if not present, staging). if the command being executed is either "install" or "info" it will default to the production host (specified by `host`). -The command-line options `--s3_host=staging` or `--s3_host=production` override the default. If `s3_host` -is present and not `staging` or `production` an exception is thrown. +To explicitly choose a host use command-line options `--s3_host=development`, `--s3_host=staging` or `--s3_host=production`, or set environment variable `node_pre_gyp_s3_host` to either `development`, `staging` or `production`. Note that the environment variable has priority over the the command line. -This allows installing from staging by specifying `--s3_host=staging`. And it requires specifying -`--s3_option=production` in order to publish to, or unpublish from, production, making accidental errors less likely. +This setup allows installing from development or staging by specifying `--s3_host=staging`. And it requires specifying `--s3_option=production` in order to publish to, or unpublish from, production, making accidental errors less likely. ## Node-API Considerations 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/main.js b/lib/main.js index bae32acb..9b54638f 100644 --- a/lib/main.js +++ b/lib/main.js @@ -72,12 +72,6 @@ function run() { return; } - // set binary.host when appropriate. host determines the s3 target bucket. - const target = prog.setBinaryHostProperty(command.name); - if (target && ['install', 'publish', 'unpublish', 'info'].indexOf(command.name) >= 0) { - log.info('using binary.host: ' + prog.package_json.binary.host); - } - prog.commands[command.name](command.args, function(err) { if (err) { log.error(command.name + ' error'); 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..6c8a1534 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', { @@ -88,11 +77,8 @@ function Run({ package_json_path = './package.json', argv }) { }); this.parseArgv(argv); - - // this is set to true after the binary.host property was set to - // either staging_host or production_host. - this.binaryHostSet = false; } + inherits(Run, EE); exports.Run = Run; const proto = Run.prototype; @@ -216,67 +202,6 @@ proto.parseArgv = function parseOpts(argv) { log.resume(); }; -/** - * allow the binary.host property to be set at execution time. - * - * for this to take effect requires all the following to be true. - * - binary is a property in package.json - * - binary.host is falsey - * - binary.staging_host is not empty - * - binary.production_host is not empty - * - * if any of the previous checks fail then the function returns an empty string - * and makes no changes to package.json's binary property. - * - * - * if command is "publish" then the default is set to "binary.staging_host" - * if command is not "publish" the the default is set to "binary.production_host" - * - * if the command-line option '--s3_host' is set to "staging" or "production" then - * "binary.host" is set to the specified "staging_host" or "production_host". if - * '--s3_host' is any other value an exception is thrown. - * - * if '--s3_host' is not present then "binary.host" is set to the default as above. - * - * this strategy was chosen so that any command other than "publish" or "unpublish" uses "production" - * as the default without requiring any command-line options but that "publish" and "unpublish" require - * '--s3_host production_host' to be specified in order to *really* publish (or unpublish). publishing - * to staging can be done freely without worrying about disturbing any production releases. - */ -proto.setBinaryHostProperty = function(command) { - if (this.binaryHostSet) { - return this.package_json.binary.host; - } - const p = this.package_json; - // don't set anything if host is present. it must be left blank to trigger this. - if (!p || !p.binary || p.binary.host) { - return ''; - } - // and both staging and production must be present. errors will be reported later. - if (!p.binary.staging_host || !p.binary.production_host) { - return ''; - } - let target = 'production_host'; - if (command === 'publish' || command === 'unpublish') { - target = 'staging_host'; - } - // the environment variable has priority over the default or the command line. if - // either the env var or the command line option are invalid throw an error. - const npg_s3_host = process.env.node_pre_gyp_s3_host; - if (npg_s3_host === 'staging' || npg_s3_host === 'production') { - target = `${npg_s3_host}_host`; - } else if (this.opts['s3_host'] === 'staging' || this.opts['s3_host'] === 'production') { - target = `${this.opts['s3_host']}_host`; - } else if (this.opts['s3_host'] || npg_s3_host) { - throw new Error(`invalid s3_host ${this.opts['s3_host'] || npg_s3_host}`); - } - - p.binary.host = p.binary[target]; - this.binaryHostSet = true; - - return p.binary.host; -}; - /** * Returns the usage instructions for node-pre-gyp. */ diff --git a/lib/pre-binding.js b/lib/pre-binding.js index e110fe38..9fd4407f 100644 --- a/lib/pre-binding.js +++ b/lib/pre-binding.js @@ -19,7 +19,6 @@ exports.find = function(package_json_path, opts) { throw new Error(package_json_path + 'does not exist'); } const prog = new npg.Run({ package_json_path, argv: process.argv }); - prog.setBinaryHostProperty(); const package_json = prog.package_json; versioning.validate_config(package_json, opts); 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/lib/util/versioning.js b/lib/util/versioning.js index 70c3f85a..656b556f 100644 --- a/lib/util/versioning.js +++ b/lib/util/versioning.js @@ -186,13 +186,51 @@ function get_runtime_abi(runtime, target_version) { } module.exports.get_runtime_abi = get_runtime_abi; -const required_parameters = [ - 'module_name', - 'module_path', - 'host' -]; +function standarize_config(package_json) { + // backwards compatibility via mutation of user supplied configuration + if (package_json.binary) { + // the option of setting production_host was introduced in + // https://github.com/mapbox/node-pre-gyp/pull/533 + // spec said that host should be falsey and production_host not empty. + // legacy config will thus have production_host (and staging_host) defined + // and will not have host defined. + // to support legacy configuration with new spec: + + // ** transfer the value of production_host to host + if (package_json.binary.production_host && !package_json.binary.host) { + package_json.binary.host = package_json.binary.production_host; + } + + if (package_json.binary.host) { + // hosts used to be specified as string (and user may still do so) + // to support legacy configuration with new spec: + + // map string format of host to object key + ['host', 'staging_host', 'development_host'].filter((item) => package_json.binary[item]).forEach((item) => { + if (typeof package_json.binary[item] === 'string') { + package_json.binary[item] = { endpoint: package_json.binary[item] }; + } + }); + + // the option to explicitly set buckt host properties was introduced in + // https://github.com/mapbox/node-pre-gyp/pull/576 + // spec defined options as keys of binary relating to the string value of host. + // legacy config will thus have bucket, region, s3ForcePathStyle defined under binary. + // to support legacy configuration with new spec: + + // map keys defined on binary to keys defined on host + ['bucket', 'region', 's3ForcePathStyle'].filter((item) => package_json.binary[item]).forEach((item) => { + if (typeof package_json.binary[item] !== 'object') { + package_json.binary.host[item] = package_json.binary[item]; + } + }); + } + } +} function validate_config(package_json, opts) { + standarize_config(package_json); // the way hosts are defined changed overtime. make it standard. + const msg = package_json.name + ' package.json is not node-pre-gyp ready:\n'; const missing = []; if (!package_json.main) { @@ -207,25 +245,40 @@ function validate_config(package_json, opts) { if (!package_json.binary) { missing.push('binary'); } - const o = package_json.binary; - if (o) { - required_parameters.forEach((p) => { - if (!o[p] || typeof o[p] !== 'string') { - missing.push('binary.' + p); + + if (package_json.binary) { + if (!package_json.binary.module_name) { + missing.push('binary.module_name'); + } + if (!package_json.binary.module_path) { + missing.push('binary.module_path'); + } + + if (!package_json.binary.host) { + missing.push('binary.host'); + } + + if (package_json.binary.host) { + if (!package_json.binary.host.endpoint) { + missing.push('binary.host.endpoint'); } - }); + } } if (missing.length >= 1) { throw new Error(msg + 'package.json must declare these properties: \n' + missing.join('\n')); } - if (o) { - // enforce https over http - const protocol = url.parse(o.host).protocol; - if (protocol === 'http:') { - throw new Error("'host' protocol (" + protocol + ") is invalid - only 'https:' is accepted"); - } + + if (package_json.binary) { + // for all possible host definitions - verify https usage + ['host', 'staging_host', 'development_host'].filter((item) => package_json.binary[item]).forEach((item) => { + const protocol = url.parse(package_json.binary[item].endpoint).protocol; + if (protocol === 'http:') { + throw new Error(msg + "'" + item + "' protocol (" + protocol + ") is invalid - only 'https:' is accepted"); + } + }); } + napi.validate_package_json(package_json, opts); } @@ -276,7 +329,8 @@ const default_remote_path = ''; module.exports.evaluate = function(package_json, options, napi_build_version) { options = options || {}; - validate_config(package_json, options); // options is a suitable substitute for opts in this case + standarize_config(package_json); // note: package_json is mutated + validate_config(package_json, options); const v = package_json.version; const module_version = semver.parse(v); const runtime = options.runtime || get_process_runtime(process.versions); @@ -304,17 +358,59 @@ module.exports.evaluate = function(package_json, options, napi_build_version) { target_arch: options.target_arch || process.arch, libc: options.target_libc || detect_libc.familySync() || 'unknown', module_main: package_json.main, - toolset: options.toolset || '', // address https://github.com/mapbox/node-pre-gyp/issues/119 - bucket: package_json.binary.bucket, - region: package_json.binary.region, - s3ForcePathStyle: package_json.binary.s3ForcePathStyle || false + toolset: options.toolset || '' // address https://github.com/mapbox/node-pre-gyp/issues/119 }; - // support host mirror with npm config `--{module_name}_binary_host_mirror` - // e.g.: https://github.com/node-inspector/v8-profiler/blob/master/package.json#L25 - // > npm install v8-profiler --profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/ + + // user can define a target host key to use (development_host, staging_host, production_host) + // by setting the name of the host (development, staging, production) + // into an environment variable or via a command line option. + // the environment variable has priority over the the command line. + let targetHost = process.env.node_pre_gyp_s3_host || options.s3_host; + + // if value is not one of the allowed silently ignore the option + if (['production', 'staging', 'development'].indexOf(targetHost) === -1) { + targetHost = ''; + } + + // the production host is as specified in 'host' key (default) + // unless there is none and alias production_host is specified (backwards compatibility) + // note: package.json is verified in validate_config to include at least one of the two. + let hostData = package_json.binary.host; + + // when a valid target is specified by user, the host is from that target (or 'host') + if (targetHost === 'production') { + // all set. catch case so as to not change host based on commands. + } + else if (targetHost === 'staging' && package_json.binary.staging_host) { + hostData = package_json.binary.staging_host; + } else if (targetHost === 'development' && package_json.binary.development_host) { + hostData = package_json.binary.development_host; + } else if ((package_json.binary.development_host || package_json.binary.staging_host)) { + // when host not specifically set via command line or environment variable + // but staging and/or development host are present in package.json + // for any command (or command chain) that includes publish or unpublish + // default to lower host (development, and if not preset, staging). + if (options.argv && options.argv.remain.some((item) => (item === 'publish' || item === 'unpublish'))) { + if (!targetHost && package_json.binary.development_host) { + hostData = package_json.binary.development_host; + } else if (package_json.binary.staging_host) { + hostData = package_json.binary.staging_host; + } + } + } + + // support host mirror with npm config `--{module_name}_binary_host_mirror` + // e.g.: https://github.com/node-inspector/v8-profiler/blob/master/package.json#L25 + // > npm install v8-profiler --profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/ const validModuleName = opts.module_name.replace('-', '_'); - const host = process.env['npm_config_' + validModuleName + '_binary_host_mirror'] || package_json.binary.host; - opts.host = fix_slashes(eval_template(host, opts)); + // explicitly set mirror overrides everything set above + hostData.endpoint = process.env['npm_config_' + validModuleName + '_binary_host_mirror'] || hostData.endpoint; + + opts.host = fix_slashes(eval_template(hostData.endpoint, opts)); + opts.bucket = hostData.bucket; + opts.region = hostData.region; + opts.s3ForcePathStyle = hostData.s3ForcePathStyle || false; + opts.module_path = eval_template(package_json.binary.module_path, opts); // now we resolve the module_path to ensure it is absolute so that binding.gyp variables work predictably if (options.module_root) { @@ -329,6 +425,7 @@ module.exports.evaluate = function(package_json, options, napi_build_version) { const package_name = package_json.binary.package_name ? package_json.binary.package_name : default_package_name; opts.package_name = eval_template(package_name, opts); opts.staged_tarball = path.join('build/stage', opts.remote_path, opts.package_name); + // when using s3ForcePathStyle the bucket is part of the http object path // add it if (opts.s3ForcePathStyle) { 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..b1edd3a6 --- /dev/null +++ b/scripts/set-bucket.js @@ -0,0 +1,60 @@ +'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', 'development_host', 'staging_host', 'production_host']; + keys.forEach((item) => { + // hosts may be specified as strings (old format) + if (pkg.binary[item] && typeof pkg.binary[item] === 'string') { + // 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]}`); + } + } + + if (pkg.binary[item] && typeof pkg.binary[item] === 'object') { + // match the bucket part of the url + const match = pkg.binary[item].endpoint.match(/^https:\/\/(.+)(?:\.s3[-.].*)$/i); + if (match) { + pkg.binary[item].endpoint = pkg.binary[item].endpoint.replace(match[1], bucket); + console.log(`Success: set ${dir} ${item} to ${pkg.binary[item]}`); + } + if (pkg.binary[item].bucket) { + pkg.binary[item].bucket = bucket; + console.log(`Set ${dir} bucket to ${pkg.binary[item].bucket}`); + } + } + }); + // bucket may be specified explicitly on binary ()old format + 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..dbfa677e --- /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 as string (legacy) 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..06a6cede --- /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 defined on the binary (legacy). 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.3/.gitignore b/test/app1.3/.gitignore new file mode 100644 index 00000000..f6a05031 --- /dev/null +++ b/test/app1.3/.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.3/README.md b/test/app1.3/README.md new file mode 100644 index 00000000..e1e26982 --- /dev/null +++ b/test/app1.3/README.md @@ -0,0 +1,4 @@ +# Test app + +Demonstrates a simple configuration that uses node-pre-gyp. +Identical to app1 but using host, staging_host and development_host configuration with an object. diff --git a/test/app1.3/app1.3.cc b/test/app1.3/app1.3.cc new file mode 100644 index 00000000..4c8a8d87 --- /dev/null +++ b/test/app1.3/app1.3.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.3/binding.gyp b/test/app1.3/binding.gyp new file mode 100644 index 00000000..05ef4f1c --- /dev/null +++ b/test/app1.3/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.3/index.js b/test/app1.3/index.js new file mode 100644 index 00000000..084fc583 --- /dev/null +++ b/test/app1.3/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.3/package.json b/test/app1.3/package.json new file mode 100644 index 00000000..cd92843f --- /dev/null +++ b/test/app1.3/package.json @@ -0,0 +1,40 @@ +{ + "name": "node-pre-gyp-test-app1.3", + "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.3", + "module_path": "./lib/binding/", + "host": { + "endpoint": "https://s3.us-east-1.amazonaws.com", + "bucket": "npg-mock-bucket", + "region": "us-east-1", + "s3ForcePathStyle": true + }, + "staging_host": { + "endpoint": "https://s3.us-east-1.amazonaws.com", + "bucket": "npg-mock-bucket", + "region": "us-east-1", + "s3ForcePathStyle": true + }, + "development_host": { + "endpoint": "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/run.test.js b/test/run.test.js index c45e900f..e121df47 100644 --- a/test/run.test.js +++ b/test/run.test.js @@ -28,10 +28,6 @@ const package_json_template = { } }; - -const all_commands = ['build', 'clean', 'configure', 'info', 'install', 'package', 'publish', 'rebuild', - 'reinstall', 'reveal', 'testbinary', 'testpackage', 'unpublish']; - /** * before testing create a scratch directory to run tests in. */ @@ -67,82 +63,6 @@ test.onFinish(() => { rimraf(scratch).then(() => undefined, () => undefined); }); -test('should set staging and production hosts', (t) => { - // make sure it's good when specifying host. - const mock_package_json = makePackageJson(); - - let { prog } = setupTest(dir, mock_package_json); - t.deepEqual(prog.package_json, mock_package_json); - t.equal(prog.binaryHostSet, false, 'binary host should not be flagged as set'); - - // test with no s3_host option - all_commands.forEach((cmd) => { - const mpj = clone(mock_package_json); - mpj.binary.host = ''; - const opts = { argv: [cmd] }; - ({ prog } = setupTest(dir, mpj, opts)); - mpj.binary.host = (cmd === 'publish' || cmd === 'unpublish') ? mpj.binary.staging_host : mpj.binary.production_host; - t.deepEqual(prog.package_json, mpj, 'host should be correct for command: ' + cmd); - t.equal(prog.binaryHostSet, true, 'binary host should be flagged as set'); - }); - - // test with s3_host set to staging - all_commands.forEach((cmd) => { - const mpj = clone(mock_package_json); - mpj.binary.host = ''; - const opts = { argv: [cmd, '--s3_host=staging'] }; - ({ prog } = setupTest(dir, mpj, opts)); - mpj.binary.host = mpj.binary.staging_host; - t.deepEqual(prog.package_json, mpj, 'host should be correct for command: ' + cmd); - t.equal(prog.binaryHostSet, true, 'binary host should be flagged as set'); - }); - - // test with s3_host set to production - all_commands.forEach((cmd) => { - const mpj = clone(mock_package_json); - mpj.binary.host = ''; - const opts = { argv: [cmd, '--s3_host=production'] }; - ({ prog } = setupTest(dir, mpj, opts)); - mpj.binary.host = mpj.binary.production_host; - t.deepEqual(prog.package_json, mpj, 'host should be correct for command: ' + cmd); - t.equal(prog.binaryHostSet, true, 'binary host should be flagged as set'); - }); - - t.end(); -}); - -test('should execute setBinaryHostProperty() properly', (t) => { - // it only --s3_host only takes effect if host is falsey. - const mock_package_json = makePackageJson({ binary: { host: '' } }); - - const opts = { argv: ['publish', '--s3_host=staging'] }; - - let { prog, binaryHost } = setupTest(dir, mock_package_json, opts); - t.equal(binaryHost, mock_package_json.binary.staging_host); - - // set it again to verify that it returns the already set value - binaryHost = prog.setBinaryHostProperty('publish'); - t.equal(binaryHost, mock_package_json.binary.staging_host); - - // now do this again but expect an empty binary host value because - // staging_host is missing. - const mpj = clone(mock_package_json); - delete mpj.binary.staging_host; - ({ prog, binaryHost } = setupTest(dir, mpj, opts)); - t.equal(binaryHost, ''); - - // one more time but with an invalid value for s3_host - opts.argv = ['publish', '--s3_host=bad-news']; - try { - ({ prog, binaryHost } = setupTest(dir, mock_package_json, opts)); - t.fail('should throw with --s3_host=bad-news'); - } catch (e) { - t.equal(e.message, 'invalid s3_host bad-news'); - } - - t.end(); -}); - test('verify that the --directory option works', (t) => { const initial = process.cwd(); @@ -223,6 +143,10 @@ test('verify that a non-existent package.json fails', (t) => { // test helpers. // +// helper to clone mock package.json. +// // https://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript +const clone = (obj) => JSON.parse(JSON.stringify(obj)); + function makePackageJson(options = {}) { const package_json = clone(package_json_template); // override binary values if supplied @@ -233,60 +157,3 @@ function makePackageJson(options = {}) { } return package_json; } - -// helper to write package.json to disk so Run() can be instantiated with it. -function setupTest(directory, package_json, opts) { - opts = opts || {}; - let argv = ['node', 'program']; - if (opts.argv) { - argv = argv.concat(opts.argv); - } - const prev_dir = process.cwd(); - if (!opts.noChdir) { - try { - fs.mkdirSync(directory); - } catch (e) { - if (e.code !== 'EEXIST') { - throw e; - } - } - process.chdir(directory); - } - - try { - fs.writeFileSync('package.json', JSON.stringify(package_json)); - const prog = new npg.Run({ package_json_path: './package.json', argv }); - const binaryHost = prog.setBinaryHostProperty(prog.todo[0] && prog.todo[0].name); - return { prog, binaryHost }; - } finally { - process.chdir(prev_dir); - } -} - -// helper to clone mock package.json. it's overkill for existing tests -// but is future-proof. -// https://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript -function clone(obj, hash = new WeakMap()) { - if (Object(obj) !== obj) return obj; // primitives - if (hash.has(obj)) return hash.get(obj); // cyclic reference - let result; - - if (obj instanceof Set) { - result = new Set(obj); // treat set as a value - } else if (obj instanceof Map) { - result = new Map(Array.from(obj, ([key, val]) => [key, clone(val, hash)])); - } else if (obj instanceof Date) { - result = new Date(obj); - } else if (obj instanceof RegExp) { - result = new RegExp(obj.source, obj.flags); - } else if (obj.constructor) { - result = new obj.constructor(); - } else { - result = Object.create(null); - } - hash.set(obj, result); - return Object.assign(result, ...Object.keys(obj).map((key) => { - return { [key]: clone(obj[key], hash) }; - })); -} - diff --git a/test/s3.test.js b/test/s3.test.js new file mode 100644 index 00000000..6b7a555a --- /dev/null +++ b/test/s3.test.js @@ -0,0 +1,236 @@ +'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': 'app1.3', + 'args': '', + 'files': { + 'base': ['binding/app1.3.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(); + }); + }); +}); diff --git a/test/versioning.test.js b/test/versioning.test.js index c59344ee..68ed0aac 100644 --- a/test/versioning.test.js +++ b/test/versioning.test.js @@ -5,28 +5,7 @@ const versioning = require('../lib/util/versioning.js'); const test = require('tape'); const detect_libc = require('detect-libc'); -test('should normalize double slash', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'test', - 'module_path': './lib/binding/{configuration}/{toolset}/{name}', - 'remote_path': './{name}/v{version}/{configuration}/{version}/{toolset}/', - 'package_name': '{module_name}-v{major}.{minor}.{patch}-{prerelease}+{build}-{toolset}-{node_abi}-{platform}-{arch}.tar.gz', - 'host': 'https://some-bucket.s3.us-east-1.amazonaws.com' - } - }; - const opts = versioning.evaluate(mock_package_json, {}); - t.equal(opts.remote_path, './test/v0.1.0/Release/0.1.0/'); - // Node v0.11.x on windows lowercases C:// when path.join is called - // https://github.com/joyent/node/issues/7031 - t.equal(path.normalize(opts.module_path), path.join(process.cwd(), 'lib/binding/Release/test')); - const opts_toolset = versioning.evaluate(mock_package_json, { toolset: 'custom-toolset' }); - t.equal(opts_toolset.remote_path, './test/v0.1.0/Release/0.1.0/custom-toolset/'); - t.end(); -}); +/* versioning */ test('should detect abi for node process', (t) => { const mock_process_versions = { @@ -35,6 +14,7 @@ test('should detect abi for node process', (t) => { modules: '11' }; const abi = versioning.get_node_abi('node', mock_process_versions); + t.equal(abi, 'node-v11'); t.equal(versioning.get_runtime_abi('node', undefined), versioning.get_node_abi('node', process.versions)); t.end(); @@ -46,19 +26,20 @@ test('should detect abi for odd node target', (t) => { modules: 'bogus' }; const abi = versioning.get_node_abi('node', mock_process_versions); + t.equal(abi, 'node-v0.11.1000000'); t.end(); }); test('should detect abi for custom node target', (t) => { const mock_process_versions = { - 'node': '0.10.0', - 'modules': '11' + node: '0.10.0', + modules: '11' }; t.equal(versioning.get_runtime_abi('node', '0.10.0'), versioning.get_node_abi('node', mock_process_versions)); const mock_process_versions2 = { - 'node': '0.8.0', - 'v8': '3.11' + node: '0.8.0', + v8: '3.11' }; t.equal(versioning.get_runtime_abi('node', '0.8.0'), versioning.get_node_abi('node', mock_process_versions2)); t.end(); @@ -66,11 +47,11 @@ test('should detect abi for custom node target', (t) => { test('should detect runtime for node-webkit and electron', (t) => { const mock_process_versions = { - 'electron': '0.37.3' + electron: '0.37.3' }; t.equal(versioning.get_process_runtime(mock_process_versions), 'electron'); const mock_process_versions2 = { - 'node': '0.8.0' + node: '0.8.0' }; t.equal(versioning.get_process_runtime(mock_process_versions2), 'node'); const mock_process_versions3 = { @@ -95,6 +76,7 @@ test('should throw when custom node target is not found in abi_crosswalk file', versioning.get_runtime_abi('node', '123456789.0.0'); } catch (e) { const expectedMessage = 'Unsupported target version: 123456789.0.0'; + t.equal(e.message, expectedMessage); t.end(); } @@ -105,47 +87,52 @@ test('should throw when custom node target is not semver', (t) => { versioning.get_runtime_abi('node', '1.2.3.4'); } catch (e) { const expectedMessage = 'Unknown target version: 1.2.3.4'; + t.equal(e.message, expectedMessage); t.end(); } }); test('should detect custom binary host from env', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'test', - 'module_path': './lib/binding/{configuration}/{toolset}/{name}', - 'remote_path': './{name}/v{version}/{configuration}/{version}/{toolset}/', - 'package_name': '{module_name}-v{major}.{minor}.{patch}-{prerelease}+{build}-{toolset}-{node_abi}-{platform}-{arch}.tar.gz', - 'host': 'https://some-bucket.s3.us-east-1.amazonaws.com' + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'test', + module_path: './lib/binding/{configuration}/{toolset}/{name}', + remote_path: './{name}/v{version}/{configuration}/{version}/{toolset}/', + package_name: '{module_name}-v{major}.{minor}.{patch}-{prerelease}+{build}-{toolset}-{node_abi}-{platform}-{arch}.tar.gz', + host: 'https://some-bucket.s3.us-east-1.amazonaws.com' } }; // mock npm_config_test_binary_host_mirror env process.env.npm_config_test_binary_host_mirror = 'https://npm.taobao.org/mirrors/node-inspector/'; - const opts = versioning.evaluate(mock_package_json, {}); + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, {}); + t.equal(opts.host, 'https://npm.taobao.org/mirrors/node-inspector/'); delete process.env.npm_config_test_binary_host_mirror; t.end(); }); test('should detect libc', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'test', - 'module_path': './lib/binding/{name}-{libc}', - 'remote_path': './{name}/{libc}/', - 'package_name': '{module_name}-{libc}.tar.gz', - 'host': 'https://some-bucket.s3-us-west-1.amazonaws.com' - } - }; - const opts = versioning.evaluate(mock_package_json, { module_root: '/root' }); + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'test', + module_path: './lib/binding/{name}-{libc}', + remote_path: './{name}/{libc}/', + package_name: '{module_name}-{libc}.tar.gz', + host: 'https://some-bucket.s3-us-west-1.amazonaws.com' + } + }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, { module_root: '/root' }); const expected_libc_token = detect_libc.familySync() || 'unknown'; + t.comment('performing test with the following libc token: ' + expected_libc_token); t.equal(opts.module_path, path.normalize('/root/lib/binding/test-' + expected_libc_token)); t.equal(opts.module, path.normalize('/root/lib/binding/test-' + expected_libc_token + '/test.node')); @@ -155,34 +142,34 @@ test('should detect libc', (t) => { t.end(); }); -// -// validate package.json versioning configurations -// +/* package.json verification */ + test('should verify that package.json has required properties', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'binary-module-name', - 'module_path': 'binary-module-path', - 'host': 'binary-path' + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path' } }; - const requireds = Object.keys(mock_package_json); + const requireds = Object.keys(parsed_package_json); for (let i = 0; i < requireds.length; i++) { - const package_json = Object.assign({}, mock_package_json); - delete package_json[requireds[i]]; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + delete cloned[requireds[i]]; const missing = [requireds[i]]; try { // eslint-disable-next-line no-unused-vars - const opts = versioning.evaluate(package_json, { module_root: '/root' }); + const opts = versioning.evaluate(cloned, {}); } catch (e) { // name won't be there if it's missing but both messages say 'undefined' - const msg = package_json.name + ' package.json is not node-pre-gyp ready:\n'; + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; const expectedMessage = msg + 'package.json must declare these properties: \n' + missing.join('\n'); + t.equal(e.message, expectedMessage); } } @@ -190,110 +177,1140 @@ test('should verify that package.json has required properties', (t) => { }); test('should verify that the binary property has required properties', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'binary-module-name', - 'module_path': 'binary-module-path', - 'host': 'binary-path' + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path' } }; - const requireds = Object.keys(mock_package_json.binary); + const requireds = Object.keys(parsed_package_json.binary); for (let i = 0; i < requireds.length; i++) { - const package_json = Object.assign({}, mock_package_json); - package_json.binary = Object.assign({}, mock_package_json.binary); - - delete package_json.binary[requireds[i]]; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + delete cloned.binary[requireds[i]]; const missing = ['binary.' + requireds[i]]; try { // eslint-disable-next-line no-unused-vars - const opts = versioning.evaluate(package_json, { module_root: '/root' }); + const opts = versioning.evaluate(cloned, {}); } catch (e) { // name won't be there if it's missing but both messages say 'undefined' - const msg = package_json.name + ' package.json is not node-pre-gyp ready:\n'; + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; const expectedMessage = msg + 'package.json must declare these properties: \n' + missing.join('\n'); + t.equal(e.message, expectedMessage); } } t.end(); }); -test('should not add bucket name to hosted_path when s3ForcePathStyle is false', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'binary-module-name', - 'module_path': 'binary-module-path', - 'host': 'binary-path', - 'bucket': 'bucket-name', - 'region': 'us-west-1', - 's3ForcePathStyle': false +test('should verify that the binary.host has required properties', (t) => { + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { + endpoint: 'binary-path' + } + } + }; + const requireds = Object.keys(parsed_package_json.binary.host); + + for (let i = 0; i < requireds.length; i++) { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + delete cloned.binary.host[requireds[i]]; + const missing = ['binary.host.' + requireds[i]]; + + try { + // eslint-disable-next-line no-unused-vars + const opts = versioning.evaluate(cloned, {}); + } catch (e) { + // name won't be there if it's missing but both messages say 'undefined' + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; + const expectedMessage = msg + 'package.json must declare these properties: \n' + missing.join('\n'); + + t.equal(e.message, expectedMessage); + } + } + t.end(); +}); + +test('should allow production_host to act as alias to host (when host not preset)', (t) => { + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + production_host: 's3-production-path' } }; - const package_json = Object.assign({}, mock_package_json); - const opts = versioning.evaluate(package_json, { module_root: '/root' }); - t.equal(opts.hosted_path, mock_package_json.binary.host + '/'); + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, {}); + t.equal(opts.host, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.production_host + '/' + opts.package_name); t.end(); }); -test('should add bucket name to hosted_path when s3ForcePathStyle is true', (t) => { - const mock_package_json = { - 'name': 'test', - 'main': 'test.js', - 'version': '0.1.0', - 'binary': { - 'module_name': 'binary-module-name', - 'module_path': 'binary-module-path', - 'host': 'binary-path', - 'bucket': 'bucket-name', - 'region': 'us-west-1', - 's3ForcePathStyle': true +test('should use host over production_host (when both are preset)', (t) => { + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + production_host: 's3-production-path', + host: 'binary-path' } }; - const package_json = Object.assign({}, mock_package_json); - const opts = versioning.evaluate(package_json, { module_root: '/root' }); - t.equal(opts.hosted_path, mock_package_json.binary.host + '/' + mock_package_json.binary.bucket + '/'); + let cloned = JSON.parse(JSON.stringify(parsed_package_json)); + let opts = versioning.evaluate(cloned, {}); + + t.equal(opts.host, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host + '/' + opts.package_name); + + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + + cloned = JSON.parse(JSON.stringify(parsed_package_json)); + opts = versioning.evaluate(cloned, {}); + + t.equal(opts.host, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host.endpoint + '/' + opts.package_name); t.end(); }); -test('should verify host overrides staging and production values', (t) => { - const mock_package_json = { +test('should verify that the host url protocol is https', (t) => { + const parsed_package_json = { name: 'test', main: 'test.js', version: '0.1.0', binary: { module_name: 'binary-module-name', module_path: 'binary-module-path', - host: 'binary-path', - staging_host: 's3-staging-path', - production_host: 's3-production-path' + host: 'http://your_module.s3-us-west-1.amazonaws.com' } }; + let cloned = JSON.parse(JSON.stringify(parsed_package_json)); + try { + // eslint-disable-next-line no-unused-vars + const opts = versioning.evaluate(cloned, {}); + } catch (e) { + // name won't be there if it's missing but both messages say 'undefined' + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; + const expectedMessage = msg + '\'host\' protocol (http:) is invalid - only \'https:\' is accepted'; + + t.equal(e.message, expectedMessage); + } + + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + cloned = JSON.parse(JSON.stringify(parsed_package_json)); try { - const opts = versioning.evaluate(mock_package_json, { module_root: '/root' }); - t.equal(opts.host, mock_package_json.binary.host + '/'); - t.equal(opts.hosted_path, mock_package_json.binary.host + '/'); - t.equal(opts.hosted_tarball, mock_package_json.binary.host + '/' + opts.package_name); + // eslint-disable-next-line no-unused-vars + const opts = versioning.evaluate(cloned, {}); } catch (e) { - t.ifError(e, 'staging_host and production_host should be silently ignored'); + // name won't be there if it's missing but both messages say 'undefined' + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; + const expectedMessage = msg + '\'host\' protocol (http:) is invalid - only \'https:\' is accepted'; + + t.equal(e.message, expectedMessage); } t.end(); }); -test('should replace "-" with "_" in custom binary host', (t) => { - const mock_package_json = { +test('should verify that alternate hosts url protocol is https', (t) => { + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'https://your_module.s3-us-west-1.amazonaws.com' + } + }; + + const hosts = ['production', 'staging', 'development']; + hosts.forEach((host) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + cloned[`${host}_host`] = `http://${host}_bucket.s3-us-west-1.amazonaws.com`; + + try { + // eslint-disable-next-line no-unused-vars + const opts = versioning.evaluate(cloned, {}); + } catch (e) { + // name won't be there if it's missing but both messages say 'undefined' + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; + const expectedMessage = msg + `'${host}_host' protocol (http:) is invalid - only 'https:' is accepted`; + + t.equal(e.message, expectedMessage); + } + }); + + hosts.forEach((host) => { + // change to object format + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + cloned.binary[`${host}_host`] = { endpoint: `http://${host}_bucket.s3-us-west-1.amazonaws.com` }; + + try { + // eslint-disable-next-line no-unused-vars + const opts = versioning.evaluate(cloned, {}); + } catch (e) { + // name won't be there if it's missing but both messages say 'undefined' + const msg = cloned.name + ' package.json is not node-pre-gyp ready:\n'; + const expectedMessage = msg + `'${host}_host' protocol (http:) is invalid - only 'https:' is accepted`; + + t.equal(e.message, expectedMessage); + } + }); + + t.end(); +}); + +/* host options */ + +test('should use host key by default for install, info, publish and unpublish commands (when no other hosts specified)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path' + } + }; + + const cmds = ['install', 'info', 'publish', 'unpublish']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use production_host as alias for host for install and info commands (when host not preset)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['install', 'info']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.production_host + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host over production_host for install and info commands (when both are preset)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + production_host: 's3-production-path', + host: 'binary-path' + } + }; + + const cmds = ['install', 'info']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host + '/' + opts.package_name); + }); + + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host by default for install and info commands (overriding alternate hosts, production_host not present)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path' + } + }; + + const cmds = ['install', 'info']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host by default for install and info commands (overriding alternate hosts, host is present)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['install', 'info']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.host = { endpoint: 'binary-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use development_host key by default for publish and unpublish commands (when it is specified)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['publish', 'unpublish']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.development_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.development_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.development_host + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.development_host = { endpoint: 's3-development-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.development_host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.development_host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.development_host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use staging_host key by default for publish and unpublish commands (when it is specified and no development_host)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['publish', 'unpublish']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.staging_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.staging_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.staging_host + '/' + opts.package_name); + + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.staging_host = { endpoint: 's3-staging-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.staging_host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.staging_host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.staging_host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use development_host key by default for publish and unpublish commands in a chain (when it is specified)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: ['info', cmd], + cooked: ['info', cmd], + original: ['info', cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['publish', 'unpublish']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.development_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.development_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.development_host + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + // change to object format + parsed_package_json.binary.development_host = { endpoint: 's3-development-path' }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.development_host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.development_host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.development_host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host specified by the --s3_host option', (t) => { + const makeOoptions = (cmd, host) => { + return { + s3_host: host, + argv: { + remain: [cmd], + cooked: [cmd, '--s3_host', host], + original: [cmd, `--s3_host=${host}`] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 's3-production-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path' + } + }; + + const hosts = ['production', 'staging', 'development']; + const cmds = ['install', 'info', 'publish', 'unpublish']; + cmds.forEach((cmd) => { + hosts.forEach((host) => { + const checkAgainst = host !== 'production' ? `${host}_host` : 'host'; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd, host)); + + t.equal(opts.host, parsed_package_json.binary[checkAgainst] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[checkAgainst] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[checkAgainst] + '/' + opts.package_name); + }); + }); + cmds.forEach((cmd) => { + hosts.forEach((host) => { + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { endpoint: 's3-production-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' } + }; + + const checkAgainst = host !== 'production' ? `${host}_host` : 'host'; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd, host)); + + t.equal(opts.host, parsed_package_json.binary[checkAgainst].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[checkAgainst].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[checkAgainst].endpoint + '/' + opts.package_name); + }); + }); + t.end(); +}); + +test('should use host specified by the --s3_host option (production_host used)', (t) => { + const makeOoptions = (cmd, host) => { + return { + s3_host: host, + argv: { + remain: [cmd], + cooked: [cmd, '--s3_host', host], + original: [cmd, `--s3_host=${host}`] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const hosts = ['production', 'staging', 'development']; + const cmds = ['install', 'info', 'publish', 'unpublish']; + cmds.forEach((cmd) => { + hosts.forEach((host) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd, host)); + + t.equal(opts.host, parsed_package_json.binary[`${host}_host`] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[`${host}_host`] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[`${host}_host`] + '/' + opts.package_name); + }); + }); + cmds.forEach((cmd) => { + hosts.forEach((host) => { + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: { endpoint: 'binary-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' }, + production_host: { endpoint: 's3-production-path' } + }; + + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd, host)); + + t.equal(opts.host, parsed_package_json.binary[`${host}_host`].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[`${host}_host`].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[`${host}_host`].endpoint + '/' + opts.package_name); + }); + }); + t.end(); +}); + +test('should use defaults when --s3_host option is invalid', (t) => { + const makeOoptions = (cmd) => { + return { + s3_host: 'not-valid', + argv: { + remain: [cmd], + cooked: [cmd, '--s3_host', 'not-valid'], + original: [cmd, '--s3_host=not-valid'] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['install', 'info', 'publish', 'unpublish']; + cmds.forEach((cmd) => { + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'development_host'; + + t.equal(opts.host, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host] + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { endpoint: 'binary-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' } + }; + + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'development_host'; + + t.equal(opts.host, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host].endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host specified by the s3_host environment variable', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const hosts = ['production', 'staging', 'development']; + const cmds = ['install', 'info', 'publish', 'unpublish']; + + cmds.forEach((cmd) => { + hosts.forEach((host) => { + process.env.node_pre_gyp_s3_host = host; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary[`${host}_host`] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[`${host}_host`] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[`${host}_host`] + '/' + opts.package_name); + }); + }); + cmds.forEach((cmd) => { + hosts.forEach((host) => { + process.env.node_pre_gyp_s3_host = host; + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: { endpoint: 'binary-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' }, + production_host: { endpoint: 's3-production-path' } + }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd, host)); + + t.equal(opts.host, parsed_package_json.binary[`${host}_host`].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[`${host}_host`].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[`${host}_host`].endpoint + '/' + opts.package_name); + }); + }); + t.end(); +}); + +test('should use defaults when s3_host environment variable is invalid', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + const cmds = ['install', 'info', 'publish', 'unpublish']; + + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'not-valid'; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'development_host'; + + t.equal(opts.host, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host] + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { endpoint: 'binary-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' } + }; + + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'development_host'; + + t.equal(opts.host, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host].endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use defaults when s3_host environment is valid but package.json does not match (production_host is default)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // no development_host + staging_host: 's3-staging-path', + // production_host not host + production_host: 's3-production-path' + } + }; + + const cmds = ['install', 'info', 'publish', 'unpublish']; + + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'development'; // specify development_host + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'production_host' : 'staging_host'; // defaults + + t.equal(opts.host, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host] + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'development'; // specify development_host + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // no development_host + staging_host: { endpoint: 's3-staging-path' }, + // production_host not host + production_host: { endpoint: 's3-production-path' } + }; + + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'production_host' : 'staging_host'; // defaults + + t.equal(opts.host, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host].endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use defaults when s3_host environment is valid but package.json does not match (host is default)', (t) => { + const makeOoptions = (cmd) => { + return { + argv: { + remain: [cmd], + cooked: [cmd], + original: [cmd] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host not production_host + host: 'binary-path', + // no development_host + staging_host: 's3-staging-path' + } + }; + + const cmds = ['install', 'info', 'publish', 'unpublish']; + + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'development'; // specify development_host + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'staging_host'; // defaults + + t.equal(opts.host, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host] + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host] + '/' + opts.package_name); + }); + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'development'; // specify development_host + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host not production_host + host: { endpoint: 'binary-path' }, + // no development_host + staging_host: { endpoint: 's3-staging-path' } + }; + + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + const host = cmd.indexOf('publish') === -1 ? 'host' : 'staging_host'; // defaults + + t.equal(opts.host, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary[host].endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary[host].endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +test('should use host specified by environment variable overriding --s3_host option', (t) => { + const makeOoptions = (cmd) => { + return { + s3_host: 'staging', // from command line + argv: { + remain: [cmd], + cooked: [cmd, '--s3_host', 'staging'], + original: [cmd, '--s3_host=staging'] + } + }; + }; + + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: 'binary-path', + development_host: 's3-development-path', + staging_host: 's3-staging-path', + production_host: 's3-production-path' + } + }; + + + const cmds = ['install', 'info', 'publish', 'unpublish']; + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'production'; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.production_host + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.production_host + '/' + opts.package_name); + }); + + cmds.forEach((cmd) => { + process.env.node_pre_gyp_s3_host = 'production'; + parsed_package_json.binary = { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + // host: { endpoint: 'binary-path' }, + development_host: { endpoint: 's3-development-path' }, + staging_host: { endpoint: 's3-staging-path' }, + production_host: { endpoint: 's3-production-path' } + }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, makeOoptions(cmd)); + + t.equal(opts.host, parsed_package_json.binary.production_host.endpoint + '/'); + t.equal(opts.hosted_path, parsed_package_json.binary.production_host.endpoint + '/'); + t.equal(opts.hosted_tarball, parsed_package_json.binary.production_host.endpoint + '/' + opts.package_name); + }); + t.end(); +}); + +/* hosted path variations */ + +test('should not add bucket name to hosted_path when s3ForcePathStyle is false', (t) => { + let parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + bucket: 'bucket-name', + region: 'us-west-1', + s3ForcePathStyle: false + } + }; + + let cloned = JSON.parse(JSON.stringify(parsed_package_json)); + let opts = versioning.evaluate(cloned, {}); + + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/'); + + // change to object format + parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { + endpoint: 'binary-path', + bucket: 'bucket-name', + region: 'us-west-1', + s3ForcePathStyle: false + } + } + }; + + cloned = JSON.parse(JSON.stringify(parsed_package_json)); + opts = versioning.evaluate(cloned, {}); + + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/'); + + t.end(); +}); + +test('should not add bucket name to hosted_path when s3ForcePathStyle is true', (t) => { + let parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: 'binary-path', + bucket: 'bucket-name', + region: 'us-west-1', + s3ForcePathStyle: true + } + }; + + let cloned = JSON.parse(JSON.stringify(parsed_package_json)); + let opts = versioning.evaluate(cloned, {}); + + t.equal(opts.hosted_path, parsed_package_json.binary.host + '/' + parsed_package_json.binary.bucket + '/'); + + // change to object format + parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'binary-module-name', + module_path: 'binary-module-path', + host: { + endpoint: 'binary-path', + bucket: 'bucket-name', + region: 'us-west-1', + s3ForcePathStyle: true + } + } + }; + + cloned = JSON.parse(JSON.stringify(parsed_package_json)); + opts = versioning.evaluate(cloned, {}); + + t.equal(opts.hosted_path, parsed_package_json.binary.host.endpoint + '/' + parsed_package_json.binary.host.bucket + '/'); + t.end(); +}); + +/* other */ + +test('should replace "-" with "_" in mirror binary host', (t) => { + const parsed_package_json = { name: 'test', main: 'test.js', version: '0.1.0', @@ -307,9 +1324,35 @@ test('should replace "-" with "_" in custom binary host', (t) => { }; process.env.npm_config_canvas_prebuilt_binary_host_mirror = 'https://npm.taobao.org/mirrors/node-canvas-prebuilt/'; - const opts = versioning.evaluate(mock_package_json, {}); + const opts = versioning.evaluate(parsed_package_json, {}); t.equal(opts.host, 'https://npm.taobao.org/mirrors/node-canvas-prebuilt/'); delete process.env.npm_config_canvas_prebuilt_binary_host_mirror; t.end(); }); +test('should normalize double slash', (t) => { + const parsed_package_json = { + name: 'test', + main: 'test.js', + version: '0.1.0', + binary: { + module_name: 'test', + module_path: './lib/binding/{configuration}/{toolset}/{name}', + remote_path: './{name}/v{version}/{configuration}/{version}/{toolset}/', + package_name: '{module_name}-v{major}.{minor}.{patch}-{prerelease}+{build}-{toolset}-{node_abi}-{platform}-{arch}.tar.gz', + host: 'https://some-bucket.s3.us-east-1.amazonaws.com' + } + }; + const cloned = JSON.parse(JSON.stringify(parsed_package_json)); + const opts = versioning.evaluate(cloned, {}); + + t.equal(opts.remote_path, './test/v0.1.0/Release/0.1.0/'); + // Node v0.11.x on windows lowercases C:// when path.join is called + // https://github.com/joyent/node/issues/7031 + t.equal(path.normalize(opts.module_path), path.join(process.cwd(), 'lib/binding/Release/test')); + + const opts_toolset = versioning.evaluate(cloned, { toolset: 'custom-toolset' }); + + t.equal(opts_toolset.remote_path, './test/v0.1.0/Release/0.1.0/custom-toolset/'); + t.end(); +});