diff --git a/package.json b/package.json index 7c65ad2838a..2272d18a5aa 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "glob": "^7.0.3", "in-publish": "^2.0.0", "is-builtin-module": "^1.0.0", - "jsdom": "^11.6.2", + "jsdom": "^11.11.0", "json-stringify-pretty-compact": "^1.0.4", "lodash": "^4.16.0", "mock-geolocation": "^1.0.11", diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 02c8bc319b2..56bdd59dfb8 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -5,7 +5,7 @@ import { extend, pick } from '../util/util'; import { getImage, ResourceType } from '../util/ajax'; import { Event, ErrorEvent, Evented } from '../util/evented'; import loadTileJSON from './load_tilejson'; -import { normalizeTileURL as normalizeURL } from '../util/mapbox'; +import { normalizeTileURL as normalizeURL, postTurnstileEvent } from '../util/mapbox'; import TileBounds from './tile_bounds'; import Texture from '../render/texture'; @@ -65,6 +65,8 @@ class RasterTileSource extends Evented implements Source { extend(this, tileJSON); if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); + postTurnstileEvent(tileJSON.tiles); + // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives // ref: https://github.com/mapbox/mapbox-gl-js/pull/4347#discussion_r104418088 diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index a253f32eac3..673b1d5a432 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -4,7 +4,7 @@ import { Event, ErrorEvent, Evented } from '../util/evented'; import { extend, pick } from '../util/util'; import loadTileJSON from './load_tilejson'; -import { normalizeTileURL as normalizeURL } from '../util/mapbox'; +import { normalizeTileURL as normalizeURL, postTurnstileEvent } from '../util/mapbox'; import TileBounds from './tile_bounds'; import { ResourceType } from '../util/ajax'; import browser from '../util/browser'; @@ -72,6 +72,8 @@ class VectorTileSource extends Evented implements Source { extend(this, tileJSON); if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); + postTurnstileEvent(tileJSON.tiles); + // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives // ref: https://github.com/mapbox/mapbox-gl-js/pull/4347#discussion_r104418088 diff --git a/src/util/ajax.js b/src/util/ajax.js index 63e96d28a60..7b730f08d7b 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -1,6 +1,7 @@ // @flow import window from './window'; +import { extend } from './util'; import type { Callback } from '../types/callback'; import type { Cancelable } from '../types/cancelable'; @@ -37,6 +38,7 @@ if (typeof Object.freeze == 'function') { export type RequestParameters = { url: string, headers?: Object, + method?: 'GET' | 'POST' | 'PUT', credentials?: 'same-origin' | 'include', collectResourceTiming?: boolean }; @@ -62,7 +64,7 @@ class AJAXError extends Error { function makeRequest(requestParameters: RequestParameters): XMLHttpRequest { const xhr: XMLHttpRequest = new window.XMLHttpRequest(); - xhr.open('GET', requestParameters.url, true); + xhr.open(requestParameters.method || 'GET', requestParameters.url, true); for (const k in requestParameters.headers) { xhr.setRequestHeader(k, requestParameters.headers[k]); } @@ -122,6 +124,23 @@ export const getArrayBuffer = function(requestParameters: RequestParameters, cal return { cancel: () => xhr.abort() }; }; +export const postData = function(requestParameters: RequestParameters, payload: string, callback: Callback): Cancelable { + const xhr = makeRequest(extend(requestParameters, {method: 'POST'})); + + xhr.onerror = function() { + callback(new Error(xhr.statusText)); + }; + xhr.onload = function() { + if (xhr.status >= 200 && xhr.status < 300) { + callback(null, xhr.response); + } else { + callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); + } + }; + xhr.send(payload); + return { cancel: () => xhr.abort() }; +}; + function sameOrigin(url) { const a: HTMLAnchorElement = window.document.createElement('a'); a.href = url; diff --git a/src/util/config.js b/src/util/config.js index c8511362f98..b15a117430c 100644 --- a/src/util/config.js +++ b/src/util/config.js @@ -2,12 +2,14 @@ type Config = {| API_URL: string, + EVENTS_URL: string, REQUIRE_ACCESS_TOKEN: boolean, ACCESS_TOKEN: ?string |}; const config: Config = { API_URL: 'https://api.mapbox.com', + EVENTS_URL: 'https://events.mapbox.com/events/v2', REQUIRE_ACCESS_TOKEN: true, ACCESS_TOKEN: null }; diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 0db851f6382..bad2e266269 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -3,8 +3,16 @@ import config from './config'; import browser from './browser'; +import window from './window'; +import { version } from '../../package.json'; +import { uuid, validateUuid, storageAvailable, warnOnce } from './util'; +import { postData } from './ajax'; + +import type { RequestParameters } from './ajax'; +import type { Cancelable } from '../types/cancelable'; const help = 'See https://www.mapbox.com/api-documentation/#access-tokens'; +const turnstileEventStorageKey = 'mapbox.turnstileEventData'; type UrlObject = {| protocol: string, @@ -119,3 +127,114 @@ function formatUrl(obj: UrlObject): string { const params = obj.params.length ? `?${obj.params.join('&')}` : ''; return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } + +export class TurnstileEvent { + eventData: { anonId: ?string, lastSuccess: ?number, accessToken: ?string}; + queue: Array; + pending: boolean + pendingRequest: ?Cancelable; + + constructor() { + this.eventData = { anonId: null, lastSuccess: null, accessToken: config.ACCESS_TOKEN}; + this.queue = []; + this.pending = false; + this.pendingRequest = null; + } + + postTurnstileEvent(tileUrls: Array) { + //Enabled only when Mapbox Access Token is set and a source uses + // mapbox tiles. + if (config.ACCESS_TOKEN && + Array.isArray(tileUrls) && + tileUrls.some((url) => { return /(mapbox\.c)(n|om)/i.test(url); })) { + this.queueRequest(browser.now()); + } + } + + queueRequest(date: number) { + this.queue.push(date); + this.processRequests(); + } + + processRequests() { + if (this.pendingRequest || this.queue.length === 0) { + return; + } + const storageKey = `${turnstileEventStorageKey}:${config.ACCESS_TOKEN || ''}`; + const isLocalStorageAvailable = storageAvailable('localStorage'); + let dueForEvent = this.eventData.accessToken ? (this.eventData.accessToken !== config.ACCESS_TOKEN) : false; + + //Reset event data cache if the access token changed. + if (dueForEvent) { + this.eventData.anonId = this.eventData.lastSuccess = null; + } + if ((!this.eventData.anonId || !this.eventData.lastSuccess) && + isLocalStorageAvailable) { + //Retrieve cached data + try { + const data = window.localStorage.getItem(storageKey); + if (data) { + this.eventData = JSON.parse(data); + } + } catch (e) { + warnOnce('Unable to read from LocalStorage'); + } + } + + if (!validateUuid(this.eventData.anonId)) { + this.eventData.anonId = uuid(); + dueForEvent = true; + } + const nextUpdate = this.queue.shift(); + + // Record turnstile event once per calendar day. + if (this.eventData.lastSuccess) { + const lastUpdate = new Date(this.eventData.lastSuccess); + const nextDate = new Date(nextUpdate); + const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); + dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); + } + + if (!dueForEvent) { + return this.processRequests(); + } + + const evenstUrlObject: UrlObject = parseUrl(config.EVENTS_URL); + evenstUrlObject.params.push(`access_token=${config.ACCESS_TOKEN || ''}`); + const request: RequestParameters = { + url: formatUrl(evenstUrlObject), + headers: { + 'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request + } + }; + + const payload = JSON.stringify([{ + event: 'appUserTurnstile', + created: (new Date(nextUpdate)).toISOString(), + sdkIdentifier: 'mapbox-gl-js', + sdkVersion: version, + 'enabled.telemetry': false, + userId: this.eventData.anonId + }]); + + this.pendingRequest = postData(request, payload, (error) => { + this.pendingRequest = null; + if (!error) { + this.eventData.lastSuccess = nextUpdate; + this.eventData.accessToken = config.ACCESS_TOKEN; + if (isLocalStorageAvailable) { + try { + window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); + } catch (e) { + warnOnce('Unable to write to LocalStorage'); + } + } + this.processRequests(); + } + }); + } +} + +const turnstileEvent_ = new TurnstileEvent(); + +export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_); diff --git a/src/util/util.js b/src/util/util.js index c96f91255d0..a06c411183b 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -4,6 +4,7 @@ import UnitBezier from '@mapbox/unitbezier'; import Coordinate from '../geo/coordinate'; import Point from '@mapbox/point-geometry'; +import window from './window'; import type {Callback} from '../types/callback'; @@ -196,6 +197,27 @@ export function uniqueId(): number { return id++; } +/** + * Return a random UUID (v4). Taken from: https://gist.github.com/jed/982883 + */ +export function uuid(): string { + function b(a) { + return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : + //$FlowFixMe: Flow doesn't like the implied array literal conversion here + ([1e7] + -[1e3] + -4e3 + -8e3 + -1e11).replace(/[018]/g, b); + } + return b(); +} + +/** + * Validate a string to match UUID(v4) of the + * form: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx + * @param str string to validate. + */ +export function validateUuid(str: ?string): boolean { + return str ? /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str) : false; +} + /** * Given an array of member function names as strings, replace all of them * with bound versions that will always refer to `context` as `this`. This @@ -440,3 +462,14 @@ export function parseCacheControl(cacheControl: string): Object { return header; } + +export function storageAvailable(type: string): boolean { + try { + const storage = window[type]; + storage.setItem('_mapbox_test_', 1); + storage.removeItem('_mapbox_test_'); + return true; + } catch (e) { + return false; + } +} diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index 7b756610a09..f633f7a88c3 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -64,6 +64,16 @@ export const getArrayBuffer = function({ url }, callback) { }); }; +export const postData = function({ url }, payload, callback) { + return request.post(url, payload, (error, response, body) => { + if (!error && response.statusCode >= 200 && response.statusCode < 300) { + callback(null, {data: body}); + } else { + callback(error || new Error(response.statusCode)); + } + }); +}; + export const getImage = function({ url }, callback) { if (cache[url]) return cached(cache[url], callback); return request({ url, encoding: null }, (error, response, body) => { diff --git a/test/unit/util/ajax.test.js b/test/unit/util/ajax.test.js index 300abef7289..4d7a1b392dd 100644 --- a/test/unit/util/ajax.test.js +++ b/test/unit/util/ajax.test.js @@ -1,7 +1,8 @@ import { test } from 'mapbox-gl-js-test'; import { getArrayBuffer, - getJSON + getJSON, + postData } from '../../../src/util/ajax'; import window from '../../../src/util/window'; @@ -97,5 +98,16 @@ test('ajax', (t) => { window.server.respond(); }); + t.test('postData, 204(no content): no error', (t) => { + window.server.respondWith(request => { + request.respond(204); + }); + postData({ url:'api.mapbox.com' }, {}, (error) => { + t.equal(error, null); + t.end(); + }); + window.server.respond(); + }); + t.end(); }); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 6dd59f8ae60..b4ee9b4fa49 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -3,11 +3,17 @@ import * as mapbox from '../../../src/util/mapbox'; import config from '../../../src/util/config'; import browser from '../../../src/util/browser'; import window from '../../../src/util/window'; +import { uuid } from '../../../src/util/util'; +import { version } from '../../../package.json'; test("mapbox", (t) => { const mapboxSource = 'mapbox://user.map'; const nonMapboxSource = 'http://www.example.com/tiles.json'; - config.ACCESS_TOKEN = 'key'; + + t.beforeEach((callback) => { + config.ACCESS_TOKEN = 'key'; + callback(); + }); t.test('.normalizeStyleURL', (t) => { t.test('returns an API URL with access_token parameter when no query string', (t) => { @@ -272,5 +278,315 @@ test("mapbox", (t) => { t.end(); }); + t.test('TurnstileEvent', (t) => { + const ms25Hours = (25 * 60 * 60 * 1000); + let event; + t.beforeEach((callback) => { + window.useFakeXMLHttpRequest(); + event = new mapbox.TurnstileEvent(); + callback(); + }); + + t.afterEach((callback) => { + window.restore(); + callback(); + }); + + t.test('mapbox.postTurnstileEvent', (t) => { + t.ok(mapbox.postTurnstileEvent); + t.end(); + }); + + t.test('does not POST when mapboxgl.ACCESS_TOKEN is not set', (t) => { + config.ACCESS_TOKEN = null; + + event.postTurnstileEvent([' a.tiles.mapxbox.com']); + + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('does not POST when url does not point to mapbox.com', (t) => { + event.postTurnstileEvent(['a.tiles.boxmap.com']); + + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('with LocalStorage available', (t) => { + let prevLocalStorage; + t.beforeEach((callback) => { + prevLocalStorage = window.localStorage; + window.localStorage = { + data: {}, + setItem: function (id, val) { + this.data[id] = String(val); + }, + getItem: function (id) { + return this.data.hasOwnProperty(id) ? this.data[id] : undefined; + }, + removeItem: function (id) { + if (this.hasOwnProperty(id)) delete this[id]; + } + }; + callback(); + }); + + t.afterEach((callback) => { + window.localStorage = prevLocalStorage; + callback(); + }); + + t.test('does not POST event when previously stored data is on the same day', (t) => { + const now = +Date.now(); + + window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: uuid(), + lastSuccess: now + })); + + t.stub(browser, 'now').callsFake(() => now + 5); // A bit later + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + t.false(window.server.requests.length); + t.end(); + }); + + t.test('POSTs event when previously stored anonId is not a valid uuid', (t) => { + const now = +Date.now(); + + window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: 'anonymous', + lastSuccess: now + })); + + t.stub(browser, 'now').callsFake(() => now + ms25Hours); // next day + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.notEqual(reqBody.userId, 'anonymous'); + t.end(); + }); + + t.test('POSTs event when previously stored timestamp is more than 24 hours in the future', (t) => { + const now = +Date.now(); + + window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: uuid(), + lastSuccess: now + ms25Hours // 24-hours later + })); + + t.stub(browser, 'now').callsFake(() => now); // Past relative ot lastSuccess + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(now).toISOString()); + + t.end(); + }); + + t.test('does not POST appuserTurnstile event second time within same calendar day', (t) => { + let now = +Date.now(); + t.stub(browser, 'now').callsFake(() => now); + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + //Post second event + const firstEvent = now; + now += (60 * 1000); // A bit later + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('does not POST appuserTurnstile event second time when clock goes backwards less than a day', (t) => { + let now = +Date.now(); + t.stub(browser, 'now').callsFake(() => now); + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + //Post second event + const firstEvent = now; + now -= (60 * 1000); // A bit earlier + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('POSTs appuserTurnstile event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.new.*`); + + t.end(); + }); + + t.end(); + }); + + t.test('when LocalStorage is not available', (t) => { + t.test('POSTs appuserTurnstile event', (t) => { + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=key`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + + t.end(); + }); + + t.test('does not POST appuserTurnstile event second time within same calendar day', (t) => { + let now = +Date.now(); + const firstEvent = now; + t.stub(browser, 'now').callsFake(() => now); + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + //Post second event + now += (60 * 1000); // A bit later + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('does not POST appuserTurnstile event second time when clock goes backwards less than a day', (t) => { + let now = +Date.now(); + const firstEvent = now; + t.stub(browser, 'now').callsFake(() => now); + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + //Post second event + now -= (60 * 1000); // A bit earlier + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('POSTs appuserTurnstile event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.new.*`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + + t.end(); + }); + + t.test('POSTs appUserTurnstile event on next calendar day', (t) => { + let now = +Date.now(); + t.stub(browser, 'now').callsFake(() => now); + + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + now += ms25Hours; // Add a day + const tomorrow = now; + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + let req = window.server.requests[0]; + req.respond(200); + + req = window.server.requests[1]; + req.respond(200); + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=key`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + t.equal(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); + + t.test('Queues and POSTs appuserTurnstile events when triggerred in quick succession', (t) => { + let now = Date.now(); + t.stub(browser, 'now').callsFake(() => now); + + const today = now; + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + const laterToday = now + 1; + now = laterToday; + event.postTurnstileEvent(['b.tiles.mapbox.com']); + + const tomorrow = laterToday + ms25Hours; // Add a day + now = tomorrow; + event.postTurnstileEvent(['c.tiles.mapbox.com']); + + const reqToday = window.server.requests[0]; + reqToday.respond(200); + let reqBody = JSON.parse(reqToday.requestBody)[0]; + t.equal(reqBody.created, new Date(today).toISOString()); + + const reqTomorrow = window.server.requests[1]; + reqTomorrow.respond(200); + reqBody = JSON.parse(reqTomorrow.requestBody)[0]; + t.equal(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); + + t.end(); + }); + + t.end(); + }); + t.end(); }); diff --git a/test/unit/util/util.test.js b/test/unit/util/util.test.js index 11dc4447228..5b4ddf6f931 100644 --- a/test/unit/util/util.test.js +++ b/test/unit/util/util.test.js @@ -3,7 +3,7 @@ import { test } from 'mapbox-gl-js-test'; import Coordinate from '../../../src/geo/coordinate'; -import { easeCubicInOut, keysDifference, extend, pick, uniqueId, getCoordinatesCenter, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl } from '../../../src/util/util'; +import { easeCubicInOut, keysDifference, extend, pick, uniqueId, getCoordinatesCenter, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid } from '../../../src/util/util'; import Point from '@mapbox/point-geometry'; test('util', (t) => { @@ -300,5 +300,13 @@ test('util', (t) => { t.end(); }); + t.test('validateUuid', (t) => { + t.true(validateUuid(uuid())); + t.false(validateUuid(uuid().substr(0, 10))); + t.false(validateUuid('foobar')); + t.false(validateUuid(null)); + t.end(); + }); + t.end(); }); diff --git a/yarn.lock b/yarn.lock index 021c6e0b878..fe375edb6ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2443,10 +2443,6 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" -content-type-parser@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" - continuable-cache@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" @@ -2710,12 +2706,18 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" -"cssstyle@>= 0.2.21 < 0.3.0", "cssstyle@>= 0.2.37 < 0.3.0": +"cssstyle@>= 0.2.21 < 0.3.0": version "0.2.37" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" dependencies: cssom "0.3.x" +"cssstyle@>= 0.3.1 < 0.4.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.3.1.tgz#6da9b4cff1bc5d716e6e5fe8e04fcb1b50a49adf" + dependencies: + cssom "0.3.x" + cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" @@ -2958,6 +2960,14 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.0.tgz#24802de4e81c298ea8a9388bb0d8e461c774684f" + dependencies: + abab "^1.0.4" + whatwg-mimetype "^2.0.0" + whatwg-url "^6.4.0" + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -5747,23 +5757,22 @@ jsdom-no-contextify@~3.1.0: xml-name-validator "^1.0.0" xmlhttprequest ">= 1.6.0 < 2.0.0" -jsdom@^11.6.2: - version "11.6.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.6.2.tgz#25d1ef332d48adf77fc5221fe2619967923f16bb" +jsdom@^11.11.0: + version "11.11.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e" dependencies: abab "^1.0.4" acorn "^5.3.0" acorn-globals "^4.1.0" array-equal "^1.0.0" - browser-process-hrtime "^0.1.2" - content-type-parser "^1.0.2" cssom ">= 0.3.2 < 0.4.0" - cssstyle ">= 0.2.37 < 0.3.0" + cssstyle ">= 0.3.1 < 0.4.0" + data-urls "^1.0.0" domexception "^1.0.0" escodegen "^1.9.0" html-encoding-sniffer "^1.0.2" left-pad "^1.2.0" - nwmatcher "^1.4.3" + nwsapi "^2.0.0" parse5 "4.0.0" pn "^1.1.0" request "^2.83.0" @@ -5774,7 +5783,8 @@ jsdom@^11.6.2: w3c-hr-time "^1.0.1" webidl-conversions "^4.0.2" whatwg-encoding "^1.0.3" - whatwg-url "^6.4.0" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" ws "^4.0.0" xml-name-validator "^3.0.0" @@ -6997,10 +7007,14 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -"nwmatcher@>= 1.3.4 < 2.0.0", nwmatcher@^1.4.3: +"nwmatcher@>= 1.3.4 < 2.0.0": version "1.4.3" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" +nwsapi@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.5.tgz#3998cfe7a014600e5e30dedb1fef2a4404b2871f" + nyc@^10.1.2: version "10.3.2" resolved "https://registry.yarnpkg.com/nyc/-/nyc-10.3.2.tgz#f27f4d91f2a9db36c24f574ff5c6efff0233de46" @@ -9990,7 +10004,7 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2. dependencies: punycode "^1.4.1" -tr46@^1.0.0: +tr46@^1.0.0, tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" dependencies: @@ -10617,6 +10631,10 @@ whatwg-fetch@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" +whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4" + whatwg-url@^6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08" @@ -10625,6 +10643,14 @@ whatwg-url@^6.4.0: tr46 "^1.0.0" webidl-conversions "^4.0.1" +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"