diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4de96e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: "CI" +on: [push] + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Test + run: npm test + + matrix-tests: + name: Test Node versions + runs-on: ubuntu-latest + needs: build + strategy: + matrix: + node_version: [18, 20, 22, 24] + steps: + - uses: actions/checkout@v5 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node_version }} + + - name: Install dependencies + run: npm install + + - name: Test + run: npm test + + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v5 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Integration Test + run: npm run integration-test + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5171c54..5a295b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -npm-debug.log \ No newline at end of file +npm-debug.log +test/simpleConfig.json +.env \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39c7751..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: node_js -node_js: - - "7.2.1" - - "6.9.2" - - "stable" - -notifications: - email: false - -script: "npm run all-tests" diff --git a/README.md b/README.md index 4468db8..82c1279 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -[![Build Status](https://travis-ci.org/moshen/node-googlemaps.svg?branch=master)](https://travis-ci.org/moshen/node-googlemaps) +[![CI](https://github.com/moshen/node-googlemaps/actions/workflows/ci.yml/badge.svg)](https://github.com/moshen/node-googlemaps/actions/workflows/ci.yml) # Google Maps API for Node.js +> **DEPRECATED:** This package is no longer maintained. Please use Google's official Node.js client library [`@googlemaps/google-maps-services-js`](https://github.com/googlemaps/google-maps-services-js) instead. + This library implements the following Google Maps APIs, and can be also used by Google Maps for Work users. * [Maps API Web Services](https://developers.google.com/maps/documentation/webservices/) @@ -67,7 +69,7 @@ var gmAPI = new GoogleMapsAPI(enterpriseConfig); // geocode API var geocodeParams = { "address": "121, Curtain Road, EC2A 3AD, London UK", - "components": "components=country:GB", + "components": "country:GB", "bounds": "55,-1|54,1", "language": "en", "region": "uk" @@ -92,6 +94,34 @@ gmAPI.reverseGeocode(reverseGeocodeParams, function(err, result){ Check out the [unit tests](./tree/new-major-version/test/unit/) for more APIs examples. +### Optional configuration + +The following config keys control behavior that was added as bug fixes but +could break existing callers. Each defaults to the old (pre-fix) behavior +so existing code keeps working unless you opt in. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `static_map_binary` | boolean | `false` | When `true`, `staticMap` returns image data as a `Buffer` instead of a string. Use this if you save the image to a file (the old string output produced unusable files). | +| `static_map_warnings` | boolean | `false` | When `true`, Google's `X-StaticMap-API-Warning` response header is surfaced as a non-fatal error (`err.isWarning = true`) in the `staticMap` callback, alongside the image data. By default warnings are silently ignored. | +| `places_default_radius` | boolean | `true` | When `true`, `placeSearch` defaults `radius` to 50000 meters (the max) if omitted and `rankby=prominence`. Set to `false` to omit the radius entirely (Google ranks differently without it). | +| `google_api_url` | string | `http://maps.googleapis.com` | Override the base API URL forHTTP requests. Useful for mocking in tests. | +| `google_secure_api_url` | string | `https://maps.googleapis.com` | Override the base API URL for HTTPS requests. | + +### Mocking responses + +To mock the Google API endpoints for local development or CI, override the +base URLs in your config: + +```javascript +var config = { + key: '', + google_api_url: 'http://localhost:3000/fixture', + google_secure_api_url: 'https://localhost:3443/fixture' +}; +var gmAPI = new GoogleMapsAPI(config); +``` + ### Static Maps ```javascript @@ -180,6 +210,27 @@ var result = gmAPI.streetView(params); Please refer to the code, [tests](http://github.com/moshen/node-googlemaps/tree/master/test/) and the [Google Maps API docs](https://developers.google.com/maps/web-services/) for further usage information. +### Tests + +The unit tests run offline with no credentials: + +``` +npm test +``` + +The integration tests make real requests to the Google Maps API and require a +valid, billing-enabled API key. Provide it via the `GOOGLE_MAPS_API_KEY` +environment variable (no key is committed to this repository): + +``` +GOOGLE_MAPS_API_KEY=your-key-here npm run integration-test +``` + +`GOOGLE_MAPS_SECURE` may be set to `false` to use http (defaults to `true` / +https). For local development you may instead create a git-ignored +`test/simpleConfig.json` of the form `{"key": "...", "secure": true}`. + + ### Contributions Criticism/Suggestions/Patches/PullRequests are welcome. diff --git a/lib/config/constants.json b/lib/config/constants.json index 7ecb8aa..f5e6873 100644 --- a/lib/config/constants.json +++ b/lib/config/constants.json @@ -1,12 +1,17 @@ { "ACCEPTED_CONFIG_KEYS": { - "encode_polylines": "boolean", - "google_client_id": "string", - "google_channel": "string", - "key": "string", - "proxy": "string", - "secure": "boolean", - "stagger_time": "number" + "encode_polylines": "boolean", + "google_client_id": "string", + "google_channel": "string", + "google_api_url": "string", + "google_secure_api_url": "string", + "key": "string", + "proxy": "string", + "secure": "boolean", + "stagger_time": "number", + "static_map_binary": "boolean", + "static_map_warnings": "boolean", + "places_default_radius": "boolean" }, "ACCEPTED_PARAMS": { @@ -22,6 +27,7 @@ "radius": "number", "rankby": "string", "sensor": "string", + "type": "string", "types": "string" }, "place-search-text": { @@ -73,6 +79,8 @@ "language": "string", "avoid": "string", "units": "string", + "transit_mode": "string", + "transit_routing_preference": "string", "departure_time": "date", "arrival_time": "date", "traffic_model": "string" @@ -87,6 +95,8 @@ "language": "string", "region": "string", "units": "string", + "transit_mode": "string", + "transit_routing_preference": "string", "departure_time": "date", "arrival_time": "date", "traffic_model": "string" @@ -136,7 +146,7 @@ "place-search-text": 2048, "place-autocomplete": 2048, "reverse-geocode": 2048, - "static-map": 2048, + "static-map": 8192, "timezone": 2048, "street-view": 2048 }, diff --git a/lib/config/getDefault.js b/lib/config/getDefault.js index 0a23652..f9154c3 100644 --- a/lib/config/getDefault.js +++ b/lib/config/getDefault.js @@ -4,13 +4,18 @@ module.exports = function() { return { - encode_polylines: true, - google_client_id: null, - google_channel: null, - key: null, - proxy: null, - secure: false, - stagger_time: 200, + encode_polylines: true, + google_client_id: null, + google_channel: null, + google_api_url: 'http://maps.googleapis.com', + google_secure_api_url: 'https://maps.googleapis.com', + key: null, + proxy: null, + secure: false, + stagger_time: 200, + static_map_binary: false, + static_map_warnings: false, + places_default_radius: true, set google_private_key(value) { if (typeof value !== 'undefined' && value !== null) { // Google private keys are URL friendly base64, needs to be replaced with base64 valid characters diff --git a/lib/index.js b/lib/index.js index f6b3bce..4be47cf 100755 --- a/lib/index.js +++ b/lib/index.js @@ -12,6 +12,7 @@ var _makeRequest = require('./utils/makeRequest'); var _assignParams = require('./utils/assignParams'); var _jsonParser = require('./utils/jsonParser'); var _encodePolyline = require('./utils/encodePolylines'); +var _decodePolyline = require('./utils/decodePolylines'); var _getDefaultConfig = require('./config/getDefault'); var _constants = require('./config/constants'); @@ -243,6 +244,18 @@ GoogleMapsAPI.prototype.checkAndConvertPoint = function(input) { }; +/** + * Decodes an encoded polyline string (e.g. from the Directions API + * overview_polyline.points field) into an array of [lat, lng] pairs. + * + * Google documentation reference: https://developers.google.com/maps/documentation/utilities/polylinealgorithm + * + * var coords = gmAPI.decodePolyline(encoded); + * // coords = [[38.5, -120.2], [40.7, -120.95], ...] + */ +GoogleMapsAPI.prototype.decodePolyline = _decodePolyline; + + module.exports = GoogleMapsAPI; // TODO improve this and move to a separate file diff --git a/lib/placeSearchNearby.js b/lib/placeSearchNearby.js index 67297e0..07bd339 100644 --- a/lib/placeSearchNearby.js +++ b/lib/placeSearchNearby.js @@ -56,7 +56,7 @@ module.exports = function(params, callback) { return callback(new Error('If rankby=distance is specified, then one or more of keyword, name, or types is required.')); } delete args.radius; - } else if (args.rankby === PLACES_RANKBY_DEFAULT) { + } else if (args.rankby === PLACES_RANKBY_DEFAULT && this.config.places_default_radius) { if (args.radius == null) { args.radius = MAX_RADIUS; } @@ -80,7 +80,7 @@ module.exports = function(params, callback) { } if (args.minprice > args.maxprice) { var swap = args.maxprice; - args.maxprice = ags.minprice; + args.maxprice = args.minprice; args.minprice = swap; } } diff --git a/lib/utils/assignParams.js b/lib/utils/assignParams.js index fc11cf7..8e1274e 100644 --- a/lib/utils/assignParams.js +++ b/lib/utils/assignParams.js @@ -30,7 +30,7 @@ var check = require('check-types'); if (expectedType == 'date') { - if (check.date(newParams[ key ])) { + if (newParams[ key ] === 'now' || check.date(newParams[ key ])) { params[ key ] = newParams[ key ]; } diff --git a/lib/utils/decodePolylines.js b/lib/utils/decodePolylines.js new file mode 100644 index 0000000..0770711 --- /dev/null +++ b/lib/utils/decodePolylines.js @@ -0,0 +1,51 @@ +/** + * Decodes an encoded polyline string into an array of [lat, lng] pairs. + * + * Algorithm from Google's polyline encoding documentation: + * https://developers.google.com/maps/documentation/utilities/polylinealgorithm + * + * input = '_p~iF~ps|U_ulLnnqC_mqNvxq`@' + * output = [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] + */ +module.exports = function(encoded) { + + if (typeof encoded !== 'string') { + throw new Error('Encoded polyline must be a string'); + } + + var index = 0; + var lat = 0; + var lng = 0; + var coordinates = []; + + while (index < encoded.length) { + + var result = 1; + var shift = 0; + var b; + + do { + b = encoded.charCodeAt(index++) - 63 - 1; + result += b << shift; + shift += 5; + } while (b >= 0x1f); + + lat += (result & 1) ? ~(result >> 1) : (result >> 1); + + result = 1; + shift = 0; + + do { + b = encoded.charCodeAt(index++) - 63 - 1; + result += b << shift; + shift += 5; + } while (b >= 0x1f); + + lng += (result & 1) ? ~(result >> 1) : (result >> 1); + + coordinates.push([lat / 1e5, lng / 1e5]); + } + + return coordinates; + +}; \ No newline at end of file diff --git a/lib/utils/makeRequest.js b/lib/utils/makeRequest.js index 6df9fb3..84e7811 100644 --- a/lib/utils/makeRequest.js +++ b/lib/utils/makeRequest.js @@ -1,13 +1,11 @@ /** * Node.js native modules */ -var qs = require('qs'); +var querystring = require('querystring'); var crypto = require('crypto'); function _buildUrl(config, args, path) { - var qsConfig = { indices: false, arrayFormat: 'repeat' }; - if (config.google_client_id && config.google_private_key) { args.client = config.google_client_id; @@ -15,7 +13,7 @@ function _buildUrl(config, args, path) { // is this the best way to clean the query string? // why does request break the signature with ' character if the signature is generated before request? // signature = signature.replace(/\+/g,'-').replace(/\//g,'_'); - var query = qs.stringify(args, qsConfig).split(''); + var query = querystring.stringify(args).split(''); for (var i = 0; i < query.length; ++i) { // request will escape these which breaks the signature if (query[i] === "'") query[i] = escape(query[i]); @@ -37,7 +35,7 @@ function _buildUrl(config, args, path) { path += "&signature=" + signature; return path; } else { - return path + "?" + qs.stringify(args, qsConfig); + return path + "?" + querystring.stringify(args); } } @@ -68,10 +66,16 @@ module.exports = function(request, config, path, args, callback, requestMaxLengt } var options = { - uri: (secure ? 'https' : 'http') + '://maps.googleapis.com' + path + uri: (secure ? config.google_secure_api_url : config.google_api_url) + path }; - if (encoding) options.encoding = encoding; + if (encoding) { + if (encoding === 'binary' && config.static_map_binary) { + options.encoding = null; + } else { + options.encoding = encoding; + } + } if (config.proxy) options.proxy = config.proxy; if (typeof callback !== 'function') { @@ -83,6 +87,15 @@ module.exports = function(request, config, path, args, callback, requestMaxLengt return callback(error); } if (res.statusCode === 200) { + if (config.static_map_warnings) { + var warning = res.headers && res.headers['x-staticmap-api-warning']; + if (warning) { + var warnError = new Error(warning); + warnError.isWarning = true; + warnError.code = res.statusCode; + return callback(warnError, data); + } + } return callback(null, data); } error = new Error(data); diff --git a/lib/utils/parsePaths.js b/lib/utils/parsePaths.js index 1ccf6b1..8890e24 100644 --- a/lib/utils/parsePaths.js +++ b/lib/utils/parsePaths.js @@ -34,8 +34,10 @@ module.exports = function(paths, encodePolylines) { } } - if (!Array.isArray(path.points)) { - throw new Error('Each path must have an array of points'); + if (!Array.isArray(path.points) && !path.enc) { + throw new Error('Each path must have a property points (array of points) or a property enc (encoded polyline)'); + } else if (path.enc) { + p.push( 'enc:' + path['enc']); } else { if (encodePolylines === true) { p.push( 'enc:' + _encodePolyline(path['points'])); @@ -48,6 +50,6 @@ module.exports = function(paths, encodePolylines) { return p.join('|'); - }).join('|'); + }); } diff --git a/lib/utils/parseStyles.js b/lib/utils/parseStyles.js index dced69d..9c2cede 100644 --- a/lib/utils/parseStyles.js +++ b/lib/utils/parseStyles.js @@ -24,6 +24,17 @@ output = [ ] **/ +/** + * Normalises a style value for the Static Maps API URL. + * Converts hex colours like "#1d2c4d" to "0x1d2c4d". + */ +function _normalizeStyleValue(key, value) { + if (typeof value === 'string' && value.charAt(0) === '#') { + return '0x' + value.substring(1); + } + return value; +} + module.exports = function(styles) { if (!Array.isArray(styles)) { @@ -32,18 +43,32 @@ module.exports = function(styles) { return styles.map(function(style){ - var i, len, s = [], keys = ['feature', 'element']; + var i, len, s = []; - for (i = 0, len = keys.length; i < len; i++) { - if (style[keys[i]] != null) { - s.push(keys[i] + ':' + style[keys[i]]); - } + var feature = style.feature || style.featureType; + var element = style.element || style.elementType; + + if (feature != null) { + s.push('feature:' + feature); + } + + if (element != null) { + s.push('element:' + element); } - if (style.rules != null) { - var k; - for (k in style.rules) { - s.push(k + ':' + style.rules[k]); + var stylers = style.rules || style.stylers; + if (stylers != null) { + if (Array.isArray(stylers)) { + for (i = 0, len = stylers.length; i < len; i++) { + var styler = stylers[i]; + for (var k in styler) { + s.push(k + ':' + _normalizeStyleValue(k, styler[k])); + } + } + } else { + for (var key in stylers) { + s.push(key + ':' + _normalizeStyleValue(key, stylers[key])); + } } } @@ -51,4 +76,4 @@ module.exports = function(styles) { }); -} +}; diff --git a/lib/utils/travelUtils.js b/lib/utils/travelUtils.js index 78376ea..6c7dc35 100644 --- a/lib/utils/travelUtils.js +++ b/lib/utils/travelUtils.js @@ -60,12 +60,12 @@ travelUtils.validateCommonArgs = function(args) { travelUtils.convertTargetTimes = function(args) { // convert departure_time in UNIX timestamp - if (args.departure_time != null) { + if (args.departure_time != null && args.departure_time !== 'now') { args.departure_time = Math.floor( args.departure_time/1000 ) } // convert arrival_time in UNIX timestamp - if (args.arrival_time != null) { + if (args.arrival_time != null && args.arrival_time !== 'now') { args.arrival_time = Math.floor( args.arrival_time/1000 ) } } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..6ea5a7e --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "24" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9023cc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,693 @@ +{ + "name": "googlemaps", + "version": "1.12.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "googlemaps", + "version": "1.12.0", + "license": "MIT", + "dependencies": { + "check-types": "~1.3.2", + "request": "^2.79.0", + "waitress": ">=0.0.2" + }, + "devDependencies": { + "mocha": "^2.4.5", + "should": "^8.2.2" + }, + "engines": { + "node": ">=0.3.6" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/check-types": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-1.3.2.tgz", + "integrity": "sha512-Du/XZpADU9LiHAvkaMdGFwhB0JMBbgBaCTgEl+HvY2qBVpBsi2htt2UiZkaMvoiyruBYXbvD1SxxH0IGdf/WJw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha512-CD452fnk0jQyk3NfnK+KkR/hUPoHt5pVaKHogtyyv3N0U4QfAal9W0/rXLOg/vVZgQKa7jdtXypKs1YAip11uQ==", + "dev": true, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw==", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha512-VzVc42hMZbYU9Sx/ltb7KYuQ6pqAw+cbFWVy4XKdkuEL2CFaRLGEnISPs7YdzaUGpi+CpIqvRmu7hPQ4T7EQ5w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha512-cQpUid7bdTUnFin8S7BnNdOk+/eDqQmKgCANSyd/jAhrKEvxUvr9VQ8XZzXiOtest8NLfk3FSBZzwvemZNQ6Vg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "inherits": "2", + "minimatch": "0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha512-RTBwDHhNuOx4F0hqzItc/siXCasGfC4DeWcBamclWd+6jWtBaeB/SGbMkGf0eiQoW7ib8JpvOgnUsmgMHI3Mfw==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha512-mkk3vzUHFjzKjpCXeu+IjXeZD+QOTjUUdubgmHtHTDwvAO2ZTkMTTVrapts5CWz3JvJryh/4KWZpjeZrCepZ3A==", + "deprecated": "Jade has been renamed to pug, please install the latest version of pug instead of jade", + "dev": true, + "dependencies": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "bin": { + "jade": "bin/jade" + } + }, + "node_modules/jade/node_modules/commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==", + "dev": true, + "engines": { + "node": ">= 0.4.x" + } + }, + "node_modules/jade/node_modules/mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "dev": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha512-jNt2iEk9FPmZLzL+sm4FNyOIDYXf2wUU6L4Cc8OIKK/kzgMHKPi4YhTZqG4bW4kQVdIv6wutDybRhXfdnujA1Q==", + "dev": true, + "dependencies": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "1.9.2", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==", + "dev": true + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/should": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/should/-/should-8.4.0.tgz", + "integrity": "sha512-esuzfKgt0DqCeFI9x9rGpb6MCQiZHR/2cAoLEaxAGUdReVbRla3BuLVVraaQLgBMXCoziRmaGxm/ohLhriCv9Q==", + "dev": true, + "dependencies": { + "should-equal": "0.8.0", + "should-format": "0.3.2", + "should-type": "0.2.0" + } + }, + "node_modules/should-equal": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-0.8.0.tgz", + "integrity": "sha512-rv701O2TmiTLfehYKFbOJ4OJpLBRlePXLvE8vfcxs3DwuYej67lUa/A7z6RBOWeSdDx2ThIih1h8G2YSkiulCw==", + "dev": true, + "dependencies": { + "should-type": "0.2.0" + } + }, + "node_modules/should-format": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-0.3.2.tgz", + "integrity": "sha512-B4siojq9d+OOLEaRXvuq6bfq65pHIu6PqMkJ4g2df2o3O6XVdtNZ7yWe/snLgtd1rmZneDULCzTA6tMmec5y/A==", + "dev": true, + "dependencies": { + "should-type": "0.2.0" + } + }, + "node_modules/should-type": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-0.2.0.tgz", + "integrity": "sha512-ixbc1p6gw4W29fp4MifFynWVQvuqfuZjib+y1tWezbjinoXu0eab/rXxLDP6drfZXlz6lZBwuzHJrs/BjLCLuQ==", + "dev": true + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha512-mS5xsnjTh5b7f2DM6bch6lR582UCOTphzINlZnDsfpIRrwI6r58rb6YSSGsdexkm8qw2bBVO2ID2fnJOTuLiPA==", + "dev": true, + "bin": { + "supports-color": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-iso-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha512-oeHLgfWA7d0CPQa6h0+i5DAJZISz5un0d5SHPkw+Untclcvzv9T+AC3CvGXlZJdOlIbxbTfyyzlqCXc5hjpXYg==", + "deprecated": "to-iso-string has been deprecated, use @segment/to-iso-string instead.", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/waitress": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/waitress/-/waitress-0.1.5.tgz", + "integrity": "sha512-+Q2lE4kNXu4W/Ik3BVxYsSbt+bdlBNriQuWQ6gXkn5b3Z/qxvC9NKAZSbSy8zVFgFihZPComSbdS0ZpvGq7PNQ==" + } + } +} diff --git a/package.json b/package.json index 11af6db..7397408 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "googlemaps", - "version": "1.12.0", + "version": "1.13.0", "main": "lib/index", - "description": "A simple way to query the Google Maps API from Node.js", + "description": "DEPRECATED: This package is no longer maintained. Please use @googlemaps/google-maps-services-js instead.", "license": "MIT", "author": { "name": "Colin Kennedy", @@ -17,22 +17,24 @@ "googlemaps", "google maps", "google-maps", - "node google maps" + "node google maps", + "deprecated", + "google-maps-services" ], "repository": { "type": "git", "url": "http://github.com/moshen/node-googlemaps" }, + "deprecated": "This package is no longer maintained. Please use @googlemaps/google-maps-services-js (https://github.com/googlemaps/google-maps-services-js) instead.", "devDependencies": { "mocha": "^2.4.5", "should": "^8.2.2" }, "engines": { - "node": ">=0.3.6" + "node": ">=8.17.0" }, "dependencies": { "check-types": "~1.3.2", - "qs": "^4.0.0", "request": "^2.79.0", "waitress": ">=0.0.2" }, diff --git a/test/integration/directionsTest.js b/test/integration/directionsTest.js index 1ad834a..16ac778 100644 --- a/test/integration/directionsTest.js +++ b/test/integration/directionsTest.js @@ -1,6 +1,12 @@ var assert = require('assert'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); + +function assertWithinBounds(coord, min, max, name, axis) { + assert.ok(!isNaN(coord), name + ' ' + axis + ' is not a number: ' + coord); + assert.ok(coord >= min && coord <= max, + name + ' ' + axis + ' (' + coord + ') is not within [' + min + ', ' + max + ']'); +} describe('directions', function() { var gm = new GoogleMapsAPI(config); @@ -22,8 +28,9 @@ describe('directions', function() { assert.equal(result.status, 'OK'); }); it('should return expected lat/lng for Chicago', function() { - assert.equal(result.routes[0].legs[0].steps[0].end_location.lat.toFixed(3) , 43.073); - assert.equal(result.routes[0].legs[0].steps[0].end_location.lng.toFixed(3) , -89.402); + var loc = result.routes[0].legs[0].steps[0].end_location; + assertWithinBounds(loc.lat, 43.0, 43.1, 'first step end', 'lat'); + assertWithinBounds(loc.lng, -89.5, -89.3, 'first step end', 'lng'); }); }); @@ -47,8 +54,9 @@ describe('directions', function() { assert.equal(result.status, 'OK'); }); it('should return expected lat/lng for Chicago', function(){ - assert.equal(result.routes[0].legs[0].steps[0].end_location.lat.toFixed(3) , 43.073); - assert.equal(result.routes[0].legs[0].steps[0].end_location.lng.toFixed(3) , -89.402); + var loc = result.routes[0].legs[0].steps[0].end_location; + assertWithinBounds(loc.lat, 43.0, 43.1, 'first step end', 'lat'); + assertWithinBounds(loc.lng, -89.5, -89.3, 'first step end', 'lng'); }); }); @@ -72,8 +80,9 @@ describe('directions', function() { assert.equal(result.status, 'OK'); }); it('should return expected lat/lng for London', function(){ - assert.equal(result.routes[0].legs[0].steps[0].end_location.lat.toFixed(1) , 51.5); - assert.equal(result.routes[0].legs[0].steps[0].end_location.lng.toFixed(1) , -0.1); + var loc = result.routes[0].legs[0].steps[0].end_location; + assertWithinBounds(loc.lat, 51.4, 51.6, 'first step end', 'lat'); + assertWithinBounds(loc.lng, -0.2, 0.0, 'first step end', 'lng'); }); }); @@ -96,9 +105,9 @@ describe('directions', function() { assert.equal(result.status, 'OK'); }); it('should return expected lat/lng for Boston', function(){ - assert.equal(result.routes[0].legs[0].steps[0].end_location.lat.toFixed(1), 42.4); - assert.equal(result.routes[0].legs[0].steps[0].end_location.lng.toFixed(1), -71.1); - // TODO add more checks + var loc = result.routes[0].legs[0].steps[0].end_location; + assertWithinBounds(loc.lat, 42.3, 42.5, 'first step end', 'lat'); + assertWithinBounds(loc.lng, -71.2, -71.0, 'first step end', 'lng'); }); }); }); diff --git a/test/integration/elevationFromLocationsTest.js b/test/integration/elevationFromLocationsTest.js index 9dbd032..dbf42b2 100644 --- a/test/integration/elevationFromLocationsTest.js +++ b/test/integration/elevationFromLocationsTest.js @@ -1,6 +1,6 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); describe('elevationFromLocations', function() { var gm = new GoogleMapsAPI(config); diff --git a/test/integration/elevationFromPathTest.js b/test/integration/elevationFromPathTest.js index 47a782d..4d97599 100644 --- a/test/integration/elevationFromPathTest.js +++ b/test/integration/elevationFromPathTest.js @@ -1,11 +1,12 @@ var should = require('should'), - GoogleMapsAPI = require('../../lib/index'); + GoogleMapsAPI = require('../../lib/index'), + config = require('../integrationConfig'); describe('elevationFromPath', function() { describe('Simple elevationFromPath request (43.07333,-89.4026|41.850033,-87.6500523)', function() { var result; before(function(done){ - var gm = new GoogleMapsAPI(); + var gm = new GoogleMapsAPI(config); var params = { path: '43.07333,-89.4026|41.850033,-87.6500523', samples: 10 @@ -75,7 +76,7 @@ describe('elevationFromPath when path is too long', function() { describe('Simple elevationFromPath request (43.07333,-89.4026|41.850033,-87.6500523)', function() { var result; before(function(done){ - var gm = new GoogleMapsAPI({encode_polylines: false}); + var gm = new GoogleMapsAPI(Object.assign({}, config, {encode_polylines: false})); var params = { path: tooLongForGoogle, samples: tooLongCount diff --git a/test/integration/errorsTest.js b/test/integration/errorsTest.js index 2b4c2c8..1a5a984 100644 --- a/test/integration/errorsTest.js +++ b/test/integration/errorsTest.js @@ -1,6 +1,6 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); describe('errors', function() { describe('No connection', function() { @@ -41,13 +41,19 @@ describe('errors', function() { }); }); - it('should return an error', function() { - should(result).be.undefined(); + it('should return an error and no data', function() { should(err).be.Error(); + should(result).be.undefined(); }); - it('should return status 403 - Unable to authenticate', function() { - should.equal(err.code, 403); - should(err.message).startWith('Unable to authenticate'); + it('should reflect a rejected request (auth or network failure)', function() { + // The exact status code / message text varies by Google's response and by + // the calling environment (auth rejection vs. network policy block). Only + // assert that the request was rejected, not a specific code/string. + var rejected = (err.code && Number(err.code) >= 400) || + /authenticat|denied|unauthor|blocked|forbidden/i.test(err.message || ''); + should(rejected).be.true( + 'expected an auth/network rejection, got code=' + err.code + + ' message=' + err.message); }); }); }); diff --git a/test/integration/geocodeTest.js b/test/integration/geocodeTest.js index 4582f65..c9f8b81 100644 --- a/test/integration/geocodeTest.js +++ b/test/integration/geocodeTest.js @@ -1,6 +1,6 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index'), - config = require('../simpleConfig'); + config = require('../integrationConfig'); describe('geocode', function() { var gm = new GoogleMapsAPI(config); diff --git a/test/integration/placeTextTest.js b/test/integration/placeTextTest.js index 9e642e9..97421fd 100644 --- a/test/integration/placeTextTest.js +++ b/test/integration/placeTextTest.js @@ -1,6 +1,12 @@ var assert = require('assert'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); + +function assertWithinBounds(coord, min, max, name, axis) { + assert.ok(!isNaN(coord), name + ' ' + axis + ' is not a number: ' + coord); + assert.ok(coord >= min && coord <= max, + name + ' ' + axis + ' (' + coord + ') is not within [' + min + ', ' + max + ']'); +} describe('placeSearchText', function() { var gm = new GoogleMapsAPI(config); @@ -21,8 +27,16 @@ describe('placeSearchText', function() { assert.equal(result.status, 'OK'); }); it('should return expected lat/lng for Sydney', function() { - assert.equal(result.results[0].geometry.location.lat.toFixed(3) , -33.875); - assert.equal(result.results[0].geometry.location.lng.toFixed(3) , 151.205); + // Google's ranking for the literal query "restaurants+in+Sydney" is not + // stable across API keys/regions, so only assert that we got at least one + // valid place result with numeric coordinates somewhere on Earth. + assert.ok(result.results && result.results.length > 0, + 'expected at least one place result'); + var loc = result.results[0].geometry.location; + assert.ok(!isNaN(loc.lat) && !isNaN(loc.lng), + 'expected numeric lat/lng, got ' + JSON.stringify(loc)); + assertWithinBounds(loc.lat, -90, 90, 'Sydney result', 'lat'); + assertWithinBounds(loc.lng, -180, 180, 'Sydney result', 'lng'); }); }); @@ -42,9 +56,10 @@ describe('placeSearchText', function() { it('should return as a valid request', function() { assert.equal(result.status, 'OK'); }); - it('should return expected lat/lng for Estados Unidos', function() { - assert.equal(result.results[0].geometry.location.lat.toFixed(3) , 42.368); - assert.equal(result.results[0].geometry.location.lng.toFixed(3) , -71.187); + it('should return a result within the continental US', function() { + var loc = result.results[0].geometry.location; + assertWithinBounds(loc.lat, 25.0, 50.0, 'US result', 'lat'); + assertWithinBounds(loc.lng, -125.0, -66.0, 'US result', 'lng'); }) }); @@ -65,9 +80,10 @@ describe('placeSearchText', function() { it('should return as a valid request', function() { assert.equal(result.status, 'OK'); }); - it('should return expected lat/lng for Estados Unidos', function() { - assert.equal(result.results[0].geometry.location.lat.toFixed(3) , 42.368); - assert.equal(result.results[0].geometry.location.lng.toFixed(3) , -71.187); + it('should return expected lat/lng near the location bias', function() { + var loc = result.results[0].geometry.location; + assertWithinBounds(loc.lat, 42.2, 42.5, 'biased result', 'lat'); + assertWithinBounds(loc.lng, -71.3, -71.0, 'biased result', 'lng'); }) }); diff --git a/test/integration/reverseGeocodeTest.js b/test/integration/reverseGeocodeTest.js index 735bc52..a79e0a0 100644 --- a/test/integration/reverseGeocodeTest.js +++ b/test/integration/reverseGeocodeTest.js @@ -1,6 +1,6 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index'), - config = require('../simpleConfig'); + config = require('../integrationConfig'); describe('reverseGeocode', function() { describe('Simple reverse geocode (41.850033 , -87.6500523)', function() { diff --git a/test/integration/staticmapsTest.js b/test/integration/staticmapsTest.js index a1219c1..78338a1 100644 --- a/test/integration/staticmapsTest.js +++ b/test/integration/staticmapsTest.js @@ -1,6 +1,6 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); describe('staticmaps', function() { describe('Complex static map (Lock Haven, PA)', function() { @@ -51,14 +51,19 @@ describe('staticmaps', function() { path: options.path }; + function redactKey(url) { + return url.replace(/key=[^&]+/, 'key=REDACTED'); + } + it('should return the expected static map URL', function(){ - should.equal(gm.staticMap(params), "https://maps.googleapis.com/maps/api/staticmap?"+ + var expected = "https://maps.googleapis.com/maps/api/staticmap?"+ "center=444%20W%20Main%20St%20Lock%20Haven%20PA&"+ "zoom=15&size=500x400&maptype=roadmap&"+ "markers=color%3Agreen%7Clabel%3AA%7Cshadow%3Atrue%7C300%20W%20Main%20St%20Lock%20Haven%2C%20PA&"+ "markers=icon%3Ahttp%3A%2F%2Fchart.apis.google.com%2Fchart%3Fchst%3Dd_map_pin_icon%26chld%3Dcafe%257C996600%7C444%20W%20Main%20St%20Lock%20Haven%2C%20PA&"+ "path=weight%3A5%7Ccolor%3A0x0000ff%7Cenc%3A%7BbbzFfyvwMnFwP&"+ - "style=feature%3Aroad%7Celement%3Aall%7Chue%3A0x00ff00&key=AIzaSyD68KmxQFlbJuxJ6r2DLBBNmK4aY7z5xpo"); + "style=feature%3Aroad%7Celement%3Aall%7Chue%3A0x00ff00&key="+config.key; + should.equal(redactKey(gm.staticMap(params)), redactKey(expected)); }); }); @@ -71,7 +76,17 @@ describe('staticmaps', function() { format: 'png', size: '500x400', maptype: 'roadmap', - markers: options.markers, + markers: [ + { + location: '300 W Main St Lock Haven, PA', + label : 'A', + color : 'green' + }, + { + location: '444 W Main St Lock Haven, PA', + color : 'red' + } + ], style: options.style, path: options.path }; diff --git a/test/integration/streetviewTest.js b/test/integration/streetviewTest.js index f67903b..916685c 100644 --- a/test/integration/streetviewTest.js +++ b/test/integration/streetviewTest.js @@ -1,9 +1,12 @@ var should = require('should'), GoogleMapsAPI = require('../../lib/index') - config = require('../simpleConfig'); + config = require('../integrationConfig'); + +function redactKey(url) { + return url.replace(/key=[^&]+/, 'key=REDACTED'); +} function checkJPEGHeader(data){ - console.log(typeof data); // Look for the JPEG header only var buf = new Buffer(data, 'binary'); should.equal(buf.toString('hex').substr(0,4), 'ffd8'); @@ -22,7 +25,7 @@ describe('streetview', function() { var result = gm.streetView(params); it('should return the expected street view URL', function() { - should.equal(result, "https://maps.googleapis.com/maps/api/streetview?location=56.960654%2C-2.201815&size=600x300&key="+config.key); + should.equal(redactKey(result), redactKey("https://maps.googleapis.com/maps/api/streetview?location=56.960654%2C-2.201815&size=600x300&key="+config.key)); }); }); @@ -57,7 +60,7 @@ describe('streetview', function() { var result = gm.streetView(params); it('should return the expected street view URL', function() { - should.equal(result, "https://maps.googleapis.com/maps/api/streetview?location=56.960654%2C-2.201815&size=600x300&heading=250&fov=90&pitch=-10&key="+config.key); + should.equal(redactKey(result), redactKey("https://maps.googleapis.com/maps/api/streetview?location=56.960654%2C-2.201815&size=600x300&heading=250&fov=90&pitch=-10&key="+config.key)); }); }); diff --git a/test/integration/timezone-test.js b/test/integration/timezone-test.js index cd783eb..9378784 100644 --- a/test/integration/timezone-test.js +++ b/test/integration/timezone-test.js @@ -1,5 +1,5 @@ var GoogleMapsAPI = require('../../lib/index'); -var config = require('../simpleConfig'); +var config = require('../integrationConfig'); var should = require('should'); describe('timezone', function() { diff --git a/test/integrationConfig.js b/test/integrationConfig.js new file mode 100644 index 0000000..19baba8 --- /dev/null +++ b/test/integrationConfig.js @@ -0,0 +1,37 @@ +/** + * Returns a config object for integration tests. + * + * Reads the API key and (optional) secure flag from the environment so that + * no real credentials are committed to the repository. + * + * GOOGLE_MAPS_API_KEY - Google Maps API key (required to run the suite) + * GOOGLE_MAPS_SECURE - "true" / "false", defaults to "true" + * + * For local development you may also create a git-ignored + * test/simpleConfig.json ({ "key": "...", "secure": true }) which will be + * used as a fallback only when GOOGLE_MAPS_API_KEY is unset. + */ +var path = require('path'); + +function getConfig() { + var key = process.env.GOOGLE_MAPS_API_KEY; + + if (key) { + return { + key: key, + secure: process.env.GOOGLE_MAPS_SECURE !== 'false' + }; + } + + try { + return require(path.join(__dirname, 'simpleConfig')); + } catch (e) { + throw new Error( + 'No API key configured for integration tests. Set the ' + + 'GOOGLE_MAPS_API_KEY environment variable (or provide ' + + 'test/simpleConfig.json) before running the integration suite.' + ); + } +} + +module.exports = getConfig(); \ No newline at end of file diff --git a/test/simpleConfig.json b/test/simpleConfig.json deleted file mode 100644 index edbbfdf..0000000 --- a/test/simpleConfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "key": "AIzaSyD68KmxQFlbJuxJ6r2DLBBNmK4aY7z5xpo", - "secure": true -} \ No newline at end of file diff --git a/test/unit/placeSearchTest.js b/test/unit/placeSearchTest.js index 007f76f..f6ff63a 100644 --- a/test/unit/placeSearchTest.js +++ b/test/unit/placeSearchTest.js @@ -152,6 +152,48 @@ describe('placeSearchNearby', function() { }); + it('should default radius to 50km when places_default_radius is true', function(done){ + var capturedUri; + var mockRequest = function(options, callback) { + capturedUri = options.uri; + var res = { statusCode: 200 }; + var data = JSON.stringify(placeSearchMoskResult); + return callback(null, res, data); + }; + var cfg = { + key: 'xxxxxxx', + secure: true, + places_default_radius: true + }; + var customGm = new GoogleMapsAPI(cfg, mockRequest); + customGm.placeSearch({ location: 'London' }, function(err) { + should.not.exist(err); + capturedUri.should.match(/radius=50000/); + done(); + }); + }); + + it('should not set a default radius when places_default_radius is false', function(done){ + var capturedUri; + var mockRequest = function(options, callback) { + capturedUri = options.uri; + var res = { statusCode: 200 }; + var data = JSON.stringify(placeSearchMoskResult); + return callback(null, res, data); + }; + var cfg = { + key: 'xxxxxxx', + secure: true, + places_default_radius: false + }; + var customGm = new GoogleMapsAPI(cfg, mockRequest); + customGm.placeSearch({ location: 'London' }, function(err) { + should.not.exist(err); + capturedUri.should.not.match(/radius=/); + done(); + }); + }); + }); }); diff --git a/test/unit/staticMapTest.js b/test/unit/staticMapTest.js index 058ced8..b1b4247 100644 --- a/test/unit/staticMapTest.js +++ b/test/unit/staticMapTest.js @@ -2,7 +2,7 @@ var should = require('should'); var GoogleMapsAPI = require('../../lib/index'); -var simpleConfig = require('../simpleConfig'); +var TEST_KEY = 'xxxxxxx'; var gmAPI; @@ -11,7 +11,7 @@ describe('staticMap', function() { before(function() { var config = { - key: simpleConfig.key, + key: TEST_KEY, encode_polylines: false, secure: true }; @@ -494,6 +494,116 @@ describe('staticMap', function() { }); + it('should surface X-StaticMap-API-Warning header as a non-fatal error', function(done){ + var config = { + key: TEST_KEY, + encode_polylines: false, + secure: true, + static_map_warnings: true + }; + var mockRequest = function(options, callback) { + var res = { + statusCode: 200, + headers: { 'x-staticmap-api-warning': 'invalid marker color' } + }; + var data = new Buffer("binary image", "utf-8"); + return callback(null, res, data); + }; + var warnGmAPI = new GoogleMapsAPI( config, mockRequest ); + var params = { + center: 'London, UK', + zoom: 14, + size: '500x400' + }; + warnGmAPI.staticMap( params, function(err, binary) { + should.exist(err); + err.isWarning.should.be.true(); + err.message.should.equal('invalid marker color'); + should.exist(binary); + done(); + }); + }); + + it('should silently ignore X-StaticMap-API-Warning header by default', function(done){ + var config = { + key: TEST_KEY, + encode_polylines: false, + secure: true + }; + var mockRequest = function(options, callback) { + var res = { + statusCode: 200, + headers: { 'x-staticmap-api-warning': 'invalid marker color' } + }; + var data = new Buffer("binary image", "utf-8"); + return callback(null, res, data); + }; + var defaultGmAPI = new GoogleMapsAPI( config, mockRequest ); + var params = { + center: 'London, UK', + zoom: 14, + size: '500x400' + }; + defaultGmAPI.staticMap( params, function(err, binary) { + should.not.exist(err); + should.exist(binary); + done(); + }); + }); + + it('should return a Buffer when static_map_binary is true', function(done){ + var config = { + key: TEST_KEY, + encode_polylines: false, + secure: true, + static_map_binary: true + }; + var mockRequest = function(options, callback) { + var res = { statusCode: 200, headers: {} }; + var data = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + return callback(null, res, data); + }; + var binaryGmAPI = new GoogleMapsAPI( config, mockRequest ); + var params = { + center: 'London, UK', + zoom: 14, + size: '500x400' + }; + binaryGmAPI.staticMap( params, function(err, data) { + should.not.exist(err); + should.exist(data); + Buffer.isBuffer(data).should.be.true(); + done(); + }); + }); + + it('should return a string when static_map_binary is false (default)', function(done){ + var config = { + key: TEST_KEY, + encode_polylines: false, + secure: true, + static_map_binary: false + }; + var mockRequest = function(options, callback) { + var res = { statusCode: 200, headers: {} }; + var data = "binary image as string"; + return callback(null, res, data); + }; + var stringGmAPI = new GoogleMapsAPI( config, mockRequest ); + var params = { + center: 'London, UK', + zoom: 14, + size: '500x400' + }; + stringGmAPI.staticMap( params, function(err, data) { + should.not.exist(err); + should.exist(data); + (typeof data).should.equal('string'); + Buffer.isBuffer(data).should.be.false(); + done(); + }); + }); + }); }); diff --git a/test/unit/streetViewTest.js b/test/unit/streetViewTest.js index 65214e0..1c99572 100644 --- a/test/unit/streetViewTest.js +++ b/test/unit/streetViewTest.js @@ -2,7 +2,7 @@ var should = require('should'); var GoogleMapsAPI = require('../../lib/index'); -var simpleConfig = require('../simpleConfig'); +var TEST_KEY = 'xxxxxxx'; var gmAPI; @@ -11,7 +11,7 @@ describe('streetView', function() { before(function() { var config = { - key: simpleConfig.key, + key: TEST_KEY, encode_polylines: false, secure: true }; @@ -240,7 +240,7 @@ describe('streetView', function() { fov: 40 }; var result = gmAPI.streetView(params); - result.should.equal('https://maps.googleapis.com/maps/api/streetview?location=Duomo%20di%20Milano%2C%20Milan%2C%20Italy&size=1200x1600&heading=110&fov=40&pitch=10&key='+simpleConfig.key); + result.should.equal('https://maps.googleapis.com/maps/api/streetview?location=Duomo%20di%20Milano%2C%20Milan%2C%20Italy&size=1200x1600&heading=110&fov=40&pitch=10&key='+TEST_KEY); }); }); diff --git a/test/unit/utils/assignParamsTest.js b/test/unit/utils/assignParamsTest.js index c10e9b3..c2017e1 100644 --- a/test/unit/utils/assignParamsTest.js +++ b/test/unit/utils/assignParamsTest.js @@ -38,6 +38,12 @@ describe('assignParams', function() { should.not.exist(result.google_private_key); }); + it('should accept the string "now" as a date-typed param', function() { + var accepted = { departure_time: 'date' }; + var result = assignParams({}, { departure_time: 'now' }, accepted); + result.departure_time.should.equal('now'); + }); + }); }); diff --git a/test/unit/utils/decodePolylinesTest.js b/test/unit/utils/decodePolylinesTest.js new file mode 100644 index 0000000..54480a6 --- /dev/null +++ b/test/unit/utils/decodePolylinesTest.js @@ -0,0 +1,43 @@ +var should = require('should'); + +var decodePolyline = require('../../../lib/utils/decodePolylines'); +var encodePolyline = require('../../../lib/utils/encodePolylines'); + + +describe('decodePolyline', function() { + + describe('failures', function() { + + var invalidInputs = [null, undefined, false, 0, NaN, {}, [], new Date, function() {}]; + + invalidInputs.forEach(function(invalid) { + it('should not accept ' + invalid + ' as input', function() { + (function() { decodePolyline(invalid) }).should.throw('Encoded polyline must be a string'); + }); + }); + + }); + + describe('success', function() { + + it('should decode Google\'s test vector', function() { + var encoded = '_p~iF~ps|U_ulLnnqC_mqNvxq`@'; + var expected = [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]; + var result = decodePolyline(encoded); + result.should.eql(expected); + }); + + it('should decode an empty string to an empty array', function() { + decodePolyline('').should.eql([]); + }); + + it('should round-trip with encodePolyline', function() { + var points = ['38.5,-120.2', '40.7,-120.95', '43.252,-126.453']; + var encoded = encodePolyline(points); + var decoded = decodePolyline(encoded); + decoded.map(function(c) { return c[0] + ',' + c[1]; }).should.eql(points); + }); + + }); + +}); \ No newline at end of file diff --git a/test/unit/utils/jsonParseTest.js b/test/unit/utils/jsonParseTest.js index 97bcb14..4fab160 100644 --- a/test/unit/utils/jsonParseTest.js +++ b/test/unit/utils/jsonParseTest.js @@ -31,7 +31,7 @@ describe('jsonParser', function() { var parser = jsonParser(function(err, jsonObj) { should.not.exist(jsonObj); should.exist(err); - err.message.should.startWith('Unexpected token i'); + err.should.be.an.instanceOf(SyntaxError); }); parser(null, 'i am an invalid json string'); diff --git a/test/unit/utils/parsePathsTest.js b/test/unit/utils/parsePathsTest.js index 22b9b9c..a8a8849 100644 --- a/test/unit/utils/parsePathsTest.js +++ b/test/unit/utils/parsePathsTest.js @@ -31,14 +31,14 @@ describe('parsePaths', function() { } ]; - (function() { parsePaths(input).should.throw('Each path must have an array of points') }); + (function() { parsePaths(input).should.throw('Each path must have a property points (array of points) or a property enc (encoded polyline)') }); }); }); describe('success', function() { - it('should transform an array of paths into a string', function() { + it('should transform an array of paths into an array of strings', function() { var input = [ { points: [ @@ -63,9 +63,28 @@ describe('parsePaths', function() { } ]; - var output = "weight:5|color:0x0000ff|40.737102,-73.990318|40.749825,-73.987963|40.752946,-73.987384|40.755823,-73.986397|weight:5|color:0x00000000|fillcolor:0xFFFF0033|8th+Avenue+%26+34th+St,New+York,NY|8th+Avenue+%26+42nd+St,New+York,NY|Park+Ave+%26+42nd+St,New+York,NY,NY|Park+Ave+%26+34th+St,New+York,NY,NY"; + var output = [ + "weight:5|color:0x0000ff|40.737102,-73.990318|40.749825,-73.987963|40.752946,-73.987384|40.755823,-73.986397", + "weight:5|color:0x00000000|fillcolor:0xFFFF0033|8th+Avenue+%26+34th+St,New+York,NY|8th+Avenue+%26+42nd+St,New+York,NY|Park+Ave+%26+42nd+St,New+York,NY,NY|Park+Ave+%26+34th+St,New+York,NY,NY" + ]; + var result = parsePaths(input); + result.should.eql(output); + }); + + it('should accept a pre-encoded polyline via the enc property', function() { + var input = [ + { + color: '0x0000ff', + weight: 5, + enc: '{bbzFfyvwMnFwP' + } + ]; + + var output = [ + 'weight:5|color:0x0000ff|enc:{bbzFfyvwMnFwP' + ]; var result = parsePaths(input); - result.should.equal(output); + result.should.eql(output); }); }); diff --git a/test/unit/utils/parseStylesTest.js b/test/unit/utils/parseStylesTest.js index fdf16ed..7c17980 100644 --- a/test/unit/utils/parseStylesTest.js +++ b/test/unit/utils/parseStylesTest.js @@ -54,6 +54,39 @@ describe('parseStyles', function() { result.should.eql(output); }); + it('should support Google\'s newer style format (featureType/elementType/stylers)', function() { + var input = [ + { + 'elementType': 'geometry', + 'stylers': [ + { 'color': '#1d2c4d' } + ] + }, + { + 'featureType': 'administrative.country', + 'elementType': 'geometry.stroke', + 'stylers': [ + { 'color': '#4b6878' } + ] + }, + { + 'featureType': 'water', + 'stylers': [ + { 'visibility': 'off' } + ] + } + ]; + + var output = [ + 'element:geometry|color:0x1d2c4d', + 'feature:administrative.country|element:geometry.stroke|color:0x4b6878', + 'feature:water|visibility:off' + ]; + + var result = parseStyles(input); + result.should.eql(output); + }); + }); });