From d377f69eb9a8598cf9b74f47f33d02a2ae135d15 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 9 Jul 2018 16:10:41 -0700 Subject: [PATCH 01/13] Ajax POST helper --- src/util/ajax.js | 25 +++++++++++++++++++++++- test/ajax_stubs.js | 12 ++++++++++++ test/unit/util/ajax.test.js | 38 ++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/util/ajax.js b/src/util/ajax.js index 63e96d28a60..4a1adf374fc 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -37,6 +37,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 +63,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 +123,28 @@ export const getArrayBuffer = function(requestParameters: RequestParameters, cal return { cancel: () => xhr.abort() }; }; +export const postData = function(requestParameters: RequestParameters, payload: string, callback: Callback): Cancelable { + requestParameters.method = 'POST'; + const xhr = makeRequest(requestParameters); + + xhr.onerror = function() { + callback(new Error(xhr.statusText)); + }; + xhr.onload = function() { + if (xhr.status >= 200 && xhr.status < 300) { + callback(null, xhr.response); + } else { + if (xhr.status === 401 && requestParameters.url.match(/mapbox.com/)) { + callback(new AJAXError(`${xhr.statusText}: you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens`, xhr.status, requestParameters.url)); + } 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/test/ajax_stubs.js b/test/ajax_stubs.js index 7b756610a09..8a1de45f3b4 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -64,6 +64,18 @@ export const getArrayBuffer = function({ url }, callback) { }); }; +export const postData = function({ url }, payload, callback) { + if (cache[url]) return cached(cache[url], callback); + return request.post(url, payload, (error, response, body) => { + if (!error && response.statusCode >= 200 && response.statusCode < 300) { + cache[url] = {data: body}; + 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..9e2182cb467 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,40 @@ 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.test('postData, 401: non-Mapbox domain', (t) => { + window.server.respondWith(request => { + request.respond(401); + }); + postData({ url:'' }, {}, (error) => { + t.equal(error.status, 401); + t.equal(error.message, "Unauthorized"); + t.end(); + }); + window.server.respond(); + }); + + t.test('postData, 401: Mapbox domain', (t) => { + window.server.respondWith(request => { + request.respond(401); + }); + postData({ url:'api.mapbox.com' }, {}, (error) => { + t.equal(error.status, 401); + t.equal(error.message, "Unauthorized: you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens"); + t.end(); + }); + window.server.respond(); + }); + t.end(); }); From ddb98982d46d4d93a980539de0eb1947574dee07 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 9 Jul 2018 11:39:43 -0700 Subject: [PATCH 02/13] Send a user Turnstile event anytime a TilJSON is used with mapbox tile servers --- src/source/raster_tile_source.js | 4 +- src/source/vector_tile_source.js | 4 +- src/util/config.js | 2 + src/util/mapbox.js | 75 ++++++++++++++++++++++++++++++++ src/util/util.js | 33 ++++++++++++++ 5 files changed, 116 insertions(+), 2 deletions(-) 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/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..92badba60e3 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -3,6 +3,12 @@ import config from './config'; import browser from './browser'; +import window from './window'; +import { version } from '../../package.json'; +import { uuid, validateUuid, storageAvailable } from './util'; +import { postData } from './ajax'; + +import type { RequestParameters } from './ajax'; const help = 'See https://www.mapbox.com/api-documentation/#access-tokens'; @@ -119,3 +125,72 @@ function formatUrl(obj: UrlObject): string { const params = obj.params.length ? `?${obj.params.join('&')}` : ''; return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } + +const STORAGE_TOKEN = 'mapbox.userTurnstileData'; +const localStorageAvailable = storageAvailable('localStorage'); + +export const postTurnstileEvent = function(tileUrls: Array) { + //Enabled only when Mapbox Access Token is set and a source uses + // mapbox tiles. + if (!config.ACCESS_TOKEN || + !tileUrls || + !tileUrls.some((url) => { return /mapbox.com/i.test(url); })) { + return; + } + + let anonId = null; + let lastUpdateTime = null; + let pending = false; + //Retrieve cached data + if (localStorageAvailable) { + const data = window.localStorage.getItem(STORAGE_TOKEN); + if (data) { + const json = JSON.parse(data); + anonId = json.anonId; + lastUpdateTime = Number(json.lastSuccess); + } + } + + if (!validateUuid(anonId)) { + anonId = uuid(); + pending = true; + } + + // Record turnstile event once per calendar day. + if (lastUpdateTime) { + const lastUpdate = new Date(lastUpdateTime); + const now = new Date(); + const daysElapsed = (+now - Number(lastUpdateTime)) / (24 * 60 * 60 * 1000); + pending = pending || daysElapsed >= 1 || daysElapsed < 0 || lastUpdate.getDate() !== now.getDate(); + } + if (!pending) { + return; + } + + 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()).toISOString(), + sdkIdentifier: 'mapbox-gl-js', + sdkVersion: `${version}`, + 'enabled.telemetry': false, + userId: anonId + }]); + + postData(request, payload, (error) => { + if (!error && localStorageAvailable) { + window.localStorage.setItem(STORAGE_TOKEN, JSON.stringify({ + lastSuccess: Date.now(), + anonId: anonId + })); + } + }); +}; diff --git a/src/util/util.js b/src/util/util.js index c96f91255d0..24d47c72151 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 + ([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; + } +} From e0cfc978d67fc943c25d59956721114148f3b2ae Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 9 Jul 2018 16:10:41 -0700 Subject: [PATCH 03/13] Unit tests for Mapbox#postTurnstileEvent --- test/unit/util/mapbox.test.js | 51 +++++++++++++++++++++++++++++++++++ test/unit/util/util.test.js | 10 ++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 6dd59f8ae60..080c0b2ef6f 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -3,6 +3,7 @@ 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 { version } from '../../../package.json'; test("mapbox", (t) => { const mapboxSource = 'mapbox://user.map'; @@ -272,5 +273,55 @@ test("mapbox", (t) => { t.end(); }); + t.test('.postTurnstileEvent', (t) => { + t.beforeEach((callback) => { + window.useFakeXMLHttpRequest(); + callback(); + }); + + t.afterEach((callback) => { + window.restore(); + callback(); + }); + + t.test('does not POST when mapboxgl.ACCESS_TOKEN is not set', (t) => { + config.ACCESS_TOKEN = null; + + mapbox.postTurnstileEvent([' a.tiles.mapxbox.com']); + + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('does not POST when urls does not point to mapbox.com', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + + mapbox.postTurnstileEvent([' a.tiles.mapxbox.cn']); + + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('POSTs appuserTurnstile event', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + window.server.respond(); + + const req = window.server.requests[0]; + const reqBody = JSON.parse(req.requestBody)[0]; + + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.*`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + + 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(); }); From 82c0e586176bdff083039ab46ac3b24fa0e4a840 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Tue, 17 Jul 2018 11:44:59 -0700 Subject: [PATCH 04/13] Include mapbox.cn endpoints --- src/util/mapbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 92badba60e3..e393d042c54 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -134,7 +134,7 @@ export const postTurnstileEvent = function(tileUrls: Array) { // mapbox tiles. if (!config.ACCESS_TOKEN || !tileUrls || - !tileUrls.some((url) => { return /mapbox.com/i.test(url); })) { + !tileUrls.some((url) => { return /mapbox.c(n)|(om)/i.test(url); })) { return; } From d96e5fd82a06bfc48e10f5dbe3d230898e820260 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Tue, 17 Jul 2018 13:10:35 -0700 Subject: [PATCH 05/13] Update jsdom to 11.11.0 to get rid of 'Error: Cross origin null forbidden' error on some machines --- package.json | 2 +- yarn.lock | 56 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 16 deletions(-) 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/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" From 790694ec87c30fb26ff6748a3df0da9e8484e813 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Tue, 17 Jul 2018 15:33:02 -0700 Subject: [PATCH 06/13] Address commments from review --- src/util/ajax.js | 6 +----- test/ajax_stubs.js | 2 -- test/unit/util/ajax.test.js | 24 ------------------------ 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/util/ajax.js b/src/util/ajax.js index 4a1adf374fc..eb773c0c3b9 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -134,11 +134,7 @@ export const postData = function(requestParameters: RequestParameters, payload: if (xhr.status >= 200 && xhr.status < 300) { callback(null, xhr.response); } else { - if (xhr.status === 401 && requestParameters.url.match(/mapbox.com/)) { - callback(new AJAXError(`${xhr.statusText}: you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens`, xhr.status, requestParameters.url)); - } else { - callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); - } + callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); } }; xhr.send(payload); diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index 8a1de45f3b4..f633f7a88c3 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -65,10 +65,8 @@ export const getArrayBuffer = function({ url }, callback) { }; export const postData = function({ url }, payload, callback) { - if (cache[url]) return cached(cache[url], callback); return request.post(url, payload, (error, response, body) => { if (!error && response.statusCode >= 200 && response.statusCode < 300) { - cache[url] = {data: body}; callback(null, {data: body}); } else { callback(error || new Error(response.statusCode)); diff --git a/test/unit/util/ajax.test.js b/test/unit/util/ajax.test.js index 9e2182cb467..4d7a1b392dd 100644 --- a/test/unit/util/ajax.test.js +++ b/test/unit/util/ajax.test.js @@ -109,29 +109,5 @@ test('ajax', (t) => { window.server.respond(); }); - t.test('postData, 401: non-Mapbox domain', (t) => { - window.server.respondWith(request => { - request.respond(401); - }); - postData({ url:'' }, {}, (error) => { - t.equal(error.status, 401); - t.equal(error.message, "Unauthorized"); - t.end(); - }); - window.server.respond(); - }); - - t.test('postData, 401: Mapbox domain', (t) => { - window.server.respondWith(request => { - request.respond(401); - }); - postData({ url:'api.mapbox.com' }, {}, (error) => { - t.equal(error.status, 401); - t.equal(error.message, "Unauthorized: you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens"); - t.end(); - }); - window.server.respond(); - }); - t.end(); }); From ccde5c77cb2c5d68d5164f54459349c0b82f48a5 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Wed, 18 Jul 2018 16:09:26 -0700 Subject: [PATCH 07/13] Use a queue and in-memory state to ensure that multiple events are not posted in quick succession --- src/util/mapbox.js | 157 +++++++++++++++++++++------------- test/unit/util/mapbox.test.js | 31 ++++--- 2 files changed, 118 insertions(+), 70 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index e393d042c54..9c1e7c2f9df 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -5,10 +5,11 @@ import config from './config'; import browser from './browser'; import window from './window'; import { version } from '../../package.json'; -import { uuid, validateUuid, storageAvailable } from './util'; +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'; @@ -126,71 +127,107 @@ function formatUrl(obj: UrlObject): string { return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } -const STORAGE_TOKEN = 'mapbox.userTurnstileData'; -const localStorageAvailable = storageAvailable('localStorage'); +class TurnstileEvent { + STORAGE_TOKEN: string; + localStorageAvailable: boolean; + eventData: Object; + queue: Array; + pending: boolean + pendingRequest: ?Cancelable; + + constructor() { + this.STORAGE_TOKEN = 'mapbox.turnstileEventData'; + this.localStorageAvailable = storageAvailable('localStorage'); + this.eventData = { anonId: null, lastSuccess: null }; + this.queue = []; + this.pending = false; + this.pendingRequest = null; + } -export const postTurnstileEvent = function(tileUrls: Array) { - //Enabled only when Mapbox Access Token is set and a source uses - // mapbox tiles. - if (!config.ACCESS_TOKEN || - !tileUrls || - !tileUrls.some((url) => { return /mapbox.c(n)|(om)/i.test(url); })) { - return; + queueRequest(date: number) { + this.queue.push(date); + this.processRequests(); } - let anonId = null; - let lastUpdateTime = null; - let pending = false; - //Retrieve cached data - if (localStorageAvailable) { - const data = window.localStorage.getItem(STORAGE_TOKEN); - if (data) { - const json = JSON.parse(data); - anonId = json.anonId; - lastUpdateTime = Number(json.lastSuccess); + processRequests() { + if (this.pending || this.queue.length === 0) { + return; + } + this.pending = true; + let dueForEvent = false; + if (!this.eventData.anonId || !this.eventData.lastSuccess) { + //Retrieve cached data + if (this.localStorageAvailable) { + try { + const data = window.localStorage.getItem(this.STORAGE_TOKEN); + if (data) { + const json = JSON.parse(data); + this.eventData.anonId = json.anonId; + this.eventData.lastSuccess = Number(json.lastSuccess); + } + } catch (e) { + warnOnce('Unable to read from LocalStorage'); + } + } } - } - if (!validateUuid(anonId)) { - anonId = uuid(); - pending = true; - } + 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 < 0 || lastUpdate.getDate() !== nextDate.getDate(); + } + if (!dueForEvent) { + return; + } - // Record turnstile event once per calendar day. - if (lastUpdateTime) { - const lastUpdate = new Date(lastUpdateTime); - const now = new Date(); - const daysElapsed = (+now - Number(lastUpdateTime)) / (24 * 60 * 60 * 1000); - pending = pending || daysElapsed >= 1 || daysElapsed < 0 || lastUpdate.getDate() !== now.getDate(); + 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) => { + if (!error && this.localStorageAvailable) { + this.eventData.lastSuccess = nextUpdate; + window.localStorage.setItem(this.STORAGE_TOKEN, JSON.stringify({ + lastSuccess: this.eventData.lastSuccess, + anonId: this.eventData.anonId + })); + this.pendingRequest = null; + this.pending = false; + this.processRequests(); + } + }); } - if (!pending) { - return; - } - - 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 turnstileEvent_ = new TurnstileEvent(); - const payload = JSON.stringify([{ - event: 'appUserTurnstile', - created: (new Date()).toISOString(), - sdkIdentifier: 'mapbox-gl-js', - sdkVersion: `${version}`, - 'enabled.telemetry': false, - userId: anonId - }]); - - postData(request, payload, (error) => { - if (!error && localStorageAvailable) { - window.localStorage.setItem(STORAGE_TOKEN, JSON.stringify({ - lastSuccess: Date.now(), - anonId: anonId - })); - } - }); +export const postTurnstileEvent = function (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); })) { + turnstileEvent_.queueRequest(Date.now()); + } }; diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 080c0b2ef6f..46d88e7de7a 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -284,6 +284,23 @@ test("mapbox", (t) => { callback(); }); + t.test('POSTs appuserTurnstile event', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + const reqBody = JSON.parse(req.requestBody)[0]; + console.log(req.url); + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.*`); + 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 when mapboxgl.ACCESS_TOKEN is not set', (t) => { config.ACCESS_TOKEN = null; @@ -296,26 +313,20 @@ test("mapbox", (t) => { t.test('does not POST when urls does not point to mapbox.com', (t) => { config.ACCESS_TOKEN = 'pk.*'; - mapbox.postTurnstileEvent([' a.tiles.mapxbox.cn']); + mapbox.postTurnstileEvent(['a.tiles.boxmap.com']); t.equal(window.server.requests.length, 0); t.end(); }); - t.test('POSTs appuserTurnstile event', (t) => { + t.test('Doesnt POST appuserTurnstile event second time', (t) => { + //Depend on having a successful POST in a prior test. config.ACCESS_TOKEN = 'pk.*'; mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); - window.server.respond(); const req = window.server.requests[0]; - const reqBody = JSON.parse(req.requestBody)[0]; - - t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.*`); - t.equal(req.method, 'POST'); - t.equal(reqBody.event, 'appUserTurnstile'); - t.equal(reqBody.sdkVersion, version); - t.ok(reqBody.userId); + t.false(req); t.end(); }); From 9c8fd965762a4b25126df06c65eb1144504dfdc3 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Fri, 20 Jul 2018 12:15:50 -0700 Subject: [PATCH 08/13] Error-ed requests should clear pending variables. Stop processing requests on XHR error. --- src/util/ajax.js | 4 ++-- src/util/mapbox.js | 37 ++++++++++++++++++++--------------- test/unit/util/mapbox.test.js | 26 ++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/util/ajax.js b/src/util/ajax.js index eb773c0c3b9..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'; @@ -124,8 +125,7 @@ export const getArrayBuffer = function(requestParameters: RequestParameters, cal }; export const postData = function(requestParameters: RequestParameters, payload: string, callback: Callback): Cancelable { - requestParameters.method = 'POST'; - const xhr = makeRequest(requestParameters); + const xhr = makeRequest(extend(requestParameters, {method: 'POST'})); xhr.onerror = function() { callback(new Error(xhr.statusText)); diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 9c1e7c2f9df..c42cc9b7cc2 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -128,16 +128,14 @@ function formatUrl(obj: UrlObject): string { } class TurnstileEvent { - STORAGE_TOKEN: string; - localStorageAvailable: boolean; + static STORAGE_TOKEN: string; + static localStorageAvailable: boolean; eventData: Object; queue: Array; pending: boolean pendingRequest: ?Cancelable; constructor() { - this.STORAGE_TOKEN = 'mapbox.turnstileEventData'; - this.localStorageAvailable = storageAvailable('localStorage'); this.eventData = { anonId: null, lastSuccess: null }; this.queue = []; this.pending = false; @@ -150,16 +148,15 @@ class TurnstileEvent { } processRequests() { - if (this.pending || this.queue.length === 0) { + if (this.pendingRequest || this.queue.length === 0) { return; } - this.pending = true; let dueForEvent = false; if (!this.eventData.anonId || !this.eventData.lastSuccess) { //Retrieve cached data - if (this.localStorageAvailable) { + if (TurnstileEvent.localStorageAvailable) { try { - const data = window.localStorage.getItem(this.STORAGE_TOKEN); + const data = window.localStorage.getItem(TurnstileEvent.STORAGE_TOKEN); if (data) { const json = JSON.parse(data); this.eventData.anonId = json.anonId; @@ -207,19 +204,27 @@ class TurnstileEvent { }]); this.pendingRequest = postData(request, payload, (error) => { - if (!error && this.localStorageAvailable) { + this.pendingRequest = null; + if (!error) { this.eventData.lastSuccess = nextUpdate; - window.localStorage.setItem(this.STORAGE_TOKEN, JSON.stringify({ - lastSuccess: this.eventData.lastSuccess, - anonId: this.eventData.anonId - })); - this.pendingRequest = null; - this.pending = false; + if (TurnstileEvent.localStorageAvailable) { + try { + window.localStorage.setItem(TurnstileEvent.STORAGE_TOKEN, JSON.stringify({ + lastSuccess: this.eventData.lastSuccess, + anonId: this.eventData.anonId + })); + } catch (e) { + warnOnce('Unable to write to LocalStorage'); + } + } this.processRequests(); } }); } } +TurnstileEvent.STORAGE_TOKEN = 'mapbox.turnstileEventData'; +TurnstileEvent.localStorageAvailable = storageAvailable('localStorage'); + const turnstileEvent_ = new TurnstileEvent(); export const postTurnstileEvent = function (tileUrls: Array) { @@ -228,6 +233,6 @@ export const postTurnstileEvent = function (tileUrls: Array) { if (config.ACCESS_TOKEN && Array.isArray(tileUrls) && tileUrls.some((url) => { return /(mapbox\.c)(n|om)/i.test(url); })) { - turnstileEvent_.queueRequest(Date.now()); + turnstileEvent_.queueRequest(browser.now()); } }; diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 46d88e7de7a..0922278ee96 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -290,8 +290,9 @@ test("mapbox", (t) => { mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); const req = window.server.requests[0]; + req.respond(200); + const reqBody = JSON.parse(req.requestBody)[0]; - console.log(req.url); t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.*`); t.equal(req.method, 'POST'); t.equal(reqBody.event, 'appUserTurnstile'); @@ -319,7 +320,7 @@ test("mapbox", (t) => { t.end(); }); - t.test('Doesnt POST appuserTurnstile event second time', (t) => { + t.test('Does not POST appuserTurnstile event second time within same calendar day', (t) => { //Depend on having a successful POST in a prior test. config.ACCESS_TOKEN = 'pk.*'; @@ -331,6 +332,27 @@ test("mapbox", (t) => { t.end(); }); + t.test('POSTs appuserTurnstile event on next calendar day', (t) => { + const today = Date.now(); + const tomorrow = today + (25 * 60 * 60 * 1000); // Add a day + t.stub(browser, 'now').callsFake(() => tomorrow); + + mapbox.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.*`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + t.ok(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); + t.end(); }); From 2c25dd80faa3ea422def4aeb1736628147f5eeb0 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 23 Jul 2018 13:20:35 -0700 Subject: [PATCH 09/13] Consider access token change as a new user to support iFrame/embed usage. --- src/util/mapbox.js | 44 ++++++++++++++----------------- test/unit/util/mapbox.test.js | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index c42cc9b7cc2..ca8a0eef687 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -12,6 +12,8 @@ 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'; +const isLocalStorageAvailable = storageAvailable('localStorage'); type UrlObject = {| protocol: string, @@ -128,15 +130,13 @@ function formatUrl(obj: UrlObject): string { } class TurnstileEvent { - static STORAGE_TOKEN: string; - static localStorageAvailable: boolean; - eventData: Object; + eventData: { anonId: ?string, lastSuccess: ?number, accessToken: ?string}; queue: Array; pending: boolean pendingRequest: ?Cancelable; constructor() { - this.eventData = { anonId: null, lastSuccess: null }; + this.eventData = { anonId: null, lastSuccess: null, accessToken: config.ACCESS_TOKEN}; this.queue = []; this.pending = false; this.pendingRequest = null; @@ -152,19 +152,16 @@ class TurnstileEvent { return; } let dueForEvent = false; - if (!this.eventData.anonId || !this.eventData.lastSuccess) { + if (!this.eventData.anonId || !this.eventData.lastSuccess && + isLocalStorageAvailable) { //Retrieve cached data - if (TurnstileEvent.localStorageAvailable) { - try { - const data = window.localStorage.getItem(TurnstileEvent.STORAGE_TOKEN); - if (data) { - const json = JSON.parse(data); - this.eventData.anonId = json.anonId; - this.eventData.lastSuccess = Number(json.lastSuccess); - } - } catch (e) { - warnOnce('Unable to read from LocalStorage'); + try { + const data = window.localStorage.getItem(turnstileEventStorageKey); + if (data) { + this.eventData = JSON.parse(data); } + } catch (e) { + warnOnce('Unable to read from LocalStorage'); } } @@ -181,8 +178,11 @@ class TurnstileEvent { const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < 0 || lastUpdate.getDate() !== nextDate.getDate(); } + + dueForEvent = dueForEvent || (this.eventData.accessToken !== config.ACCESS_TOKEN); + if (!dueForEvent) { - return; + return this.processRequests(); } const evenstUrlObject: UrlObject = parseUrl(config.EVENTS_URL); @@ -198,7 +198,7 @@ class TurnstileEvent { event: 'appUserTurnstile', created: (new Date(nextUpdate)).toISOString(), sdkIdentifier: 'mapbox-gl-js', - sdkVersion: `${version}`, + sdkVersion: version, 'enabled.telemetry': false, userId: this.eventData.anonId }]); @@ -207,12 +207,10 @@ class TurnstileEvent { this.pendingRequest = null; if (!error) { this.eventData.lastSuccess = nextUpdate; - if (TurnstileEvent.localStorageAvailable) { + this.eventData.accessToken = config.ACCESS_TOKEN; + if (isLocalStorageAvailable) { try { - window.localStorage.setItem(TurnstileEvent.STORAGE_TOKEN, JSON.stringify({ - lastSuccess: this.eventData.lastSuccess, - anonId: this.eventData.anonId - })); + window.localStorage.setItem(turnstileEventStorageKey, this.eventData); } catch (e) { warnOnce('Unable to write to LocalStorage'); } @@ -222,8 +220,6 @@ class TurnstileEvent { }); } } -TurnstileEvent.STORAGE_TOKEN = 'mapbox.turnstileEventData'; -TurnstileEvent.localStorageAvailable = storageAvailable('localStorage'); const turnstileEvent_ = new TurnstileEvent(); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 0922278ee96..02be60591b2 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -285,6 +285,7 @@ test("mapbox", (t) => { }); t.test('POSTs appuserTurnstile event', (t) => { + t.stub(console, 'warn'); config.ACCESS_TOKEN = 'pk.*'; mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); @@ -299,6 +300,7 @@ test("mapbox", (t) => { t.equal(reqBody.sdkVersion, version); t.ok(reqBody.userId); + t.ok(console.warn.calledOnce); t.end(); }); @@ -332,7 +334,26 @@ test("mapbox", (t) => { t.end(); }); + t.test('POSTs appuserTurnstile event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + mapbox.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) => { + config.ACCESS_TOKEN = 'pk.*'; const today = Date.now(); const tomorrow = today + (25 * 60 * 60 * 1000); // Add a day t.stub(browser, 'now').callsFake(() => tomorrow); @@ -353,6 +374,34 @@ test("mapbox", (t) => { t.end(); }); + t.test('POSTs consecutive appuserTurnstile events on different calendar days', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + const today = Date.now(); + const laterToday = today + 1; + const tomorrow = today + (25 * 60 * 60 * 1000); // Add a day + + const stub = t.stub(browser, 'now'); + stub.onCall(0).returns(today); + stub.onCall(1).returns(laterToday); + stub.onCall(2).returns(tomorrow); + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + mapbox.postTurnstileEvent(['b.tiles.mapbox.com']); + mapbox.postTurnstileEvent(['c.tiles.mapbox.com']); + + const reqToday = window.server.requests[0]; + reqToday.respond(200); + let reqBody = JSON.parse(reqToday.requestBody)[0]; + t.ok(reqBody.created, new Date(today).toISOString()); + + const reqTomorrow = window.server.requests[1]; + reqTomorrow.respond(200); + reqBody = JSON.parse(reqTomorrow.requestBody)[0]; + t.ok(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); + t.end(); }); From ab94794b5a354d4805f6d1875c3948fb78082636 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 23 Jul 2018 16:13:37 -0700 Subject: [PATCH 10/13] Segment locally stored data by access token to allow multiple embeds from same source url with different tokens. --- src/util/mapbox.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index ca8a0eef687..bbda96b292c 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -151,12 +151,18 @@ class TurnstileEvent { if (this.pendingRequest || this.queue.length === 0) { return; } - let dueForEvent = false; + const storageKey = `${turnstileEventStorageKey}:${config.ACCESS_TOKEN || ''}`; + let dueForEvent = (this.eventData.accessToken !== config.ACCESS_TOKEN); + + //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(turnstileEventStorageKey); + const data = window.localStorage.getItem(storageKey); if (data) { this.eventData = JSON.parse(data); } @@ -179,8 +185,6 @@ class TurnstileEvent { dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < 0 || lastUpdate.getDate() !== nextDate.getDate(); } - dueForEvent = dueForEvent || (this.eventData.accessToken !== config.ACCESS_TOKEN); - if (!dueForEvent) { return this.processRequests(); } @@ -210,7 +214,7 @@ class TurnstileEvent { this.eventData.accessToken = config.ACCESS_TOKEN; if (isLocalStorageAvailable) { try { - window.localStorage.setItem(turnstileEventStorageKey, this.eventData); + window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); } catch (e) { warnOnce('Unable to write to LocalStorage'); } From b0e18e4563d4fc70273a8215cfe36e53df1e9e33 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Fri, 27 Jul 2018 12:02:55 -0700 Subject: [PATCH 11/13] Add unit tests with mock LocalStorage --- src/util/mapbox.js | 6 +- src/util/util.js | 2 +- test/unit/util/mapbox.test.js | 281 ++++++++++++++++++++++++---------- 3 files changed, 204 insertions(+), 85 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index bbda96b292c..dc1060f877d 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -13,7 +13,6 @@ import type { Cancelable } from '../types/cancelable'; const help = 'See https://www.mapbox.com/api-documentation/#access-tokens'; const turnstileEventStorageKey = 'mapbox.turnstileEventData'; -const isLocalStorageAvailable = storageAvailable('localStorage'); type UrlObject = {| protocol: string, @@ -152,13 +151,14 @@ class TurnstileEvent { return; } const storageKey = `${turnstileEventStorageKey}:${config.ACCESS_TOKEN || ''}`; - let dueForEvent = (this.eventData.accessToken !== 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 && + if ((!this.eventData.anonId || !this.eventData.lastSuccess) && isLocalStorageAvailable) { //Retrieve cached data try { diff --git a/src/util/util.js b/src/util/util.js index 24d47c72151..a06c411183b 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -203,7 +203,7 @@ export function uniqueId(): number { export function uuid(): string { function b(a) { return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : - //$FlowFixMe + //$FlowFixMe: Flow doesn't like the implied array literal conversion here ([1e7] + -[1e3] + -4e3 + -8e3 + -1e11).replace(/[018]/g, b); } return b(); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 02be60591b2..9b80e279a5f 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -3,6 +3,7 @@ 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) => { @@ -274,6 +275,7 @@ test("mapbox", (t) => { }); t.test('.postTurnstileEvent', (t) => { + const ms25Hours = (25 * 60 * 60 * 1000); t.beforeEach((callback) => { window.useFakeXMLHttpRequest(); callback(); @@ -284,26 +286,6 @@ test("mapbox", (t) => { callback(); }); - t.test('POSTs appuserTurnstile event', (t) => { - t.stub(console, 'warn'); - config.ACCESS_TOKEN = 'pk.*'; - - mapbox.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.*`); - t.equal(req.method, 'POST'); - t.equal(reqBody.event, 'appUserTurnstile'); - t.equal(reqBody.sdkVersion, version); - t.ok(reqBody.userId); - - t.ok(console.warn.calledOnce); - t.end(); - }); - t.test('does not POST when mapboxgl.ACCESS_TOKEN is not set', (t) => { config.ACCESS_TOKEN = null; @@ -313,7 +295,7 @@ test("mapbox", (t) => { t.end(); }); - t.test('does not POST when urls does not point to mapbox.com', (t) => { + t.test('does not POST when url does not point to mapbox.com', (t) => { config.ACCESS_TOKEN = 'pk.*'; mapbox.postTurnstileEvent(['a.tiles.boxmap.com']); @@ -322,82 +304,219 @@ test("mapbox", (t) => { t.end(); }); - t.test('Does not POST appuserTurnstile event second time within same calendar day', (t) => { - //Depend on having a successful POST in a prior test. - config.ACCESS_TOKEN = 'pk.*'; + 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(); + }); - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + t.afterEach((callback) => { + window.localStorage = prevLocalStorage; + callback(); + }); - const req = window.server.requests[0]; - t.false(req); + t.test('does not POST event when previously stored data is on the same day', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + const now = +Date.now(); - t.end(); - }); + window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: uuid(), + lastSuccess: now + })); - t.test('POSTs appuserTurnstile event when access token changes', (t) => { - config.ACCESS_TOKEN = 'pk.new.*'; + t.stub(browser, 'now').callsFake(() => now + 5); // A bit later - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); - const req = window.server.requests[0]; - req.respond(200); + t.false(window.server.requests.length); + t.end(); + }); - 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.test('POSTs event when previously stored anonId is not a valid uuid', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + const now = +Date.now(); - t.end(); - }); + window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: 'anonymous', + lastSuccess: now + })); - t.test('POSTs appuserTurnstile event on next calendar day', (t) => { - config.ACCESS_TOKEN = 'pk.*'; - const today = Date.now(); - const tomorrow = today + (25 * 60 * 60 * 1000); // Add a day - t.stub(browser, 'now').callsFake(() => tomorrow); + t.stub(browser, 'now').callsFake(() => now + ms25Hours); // next day + + mapbox.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 in the future', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + 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 + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + req.respond(200); + const reqBody = JSON.parse(req.requestBody)[0]; + t.ok(reqBody.created, new Date(now).toISOString()); + + t.end(); + }); + + t.test('does not POST appuserTurnstile event second time within same calendar day', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + + t.stub(browser, 'now').callsFake(() => +Date.now() + (60 * 1000)); // A bit later + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + t.false(req); + + t.end(); + }); + + t.test('POSTs appuserTurnstile event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); - const req = window.server.requests[0]; - req.respond(200); + 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.*`); - t.equal(req.method, 'POST'); - t.equal(reqBody.event, 'appUserTurnstile'); - t.equal(reqBody.sdkVersion, version); - t.ok(reqBody.userId); - t.ok(reqBody.created, new Date(tomorrow).toISOString()); + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.new.*`); + + t.end(); + }); t.end(); }); - t.test('POSTs consecutive appuserTurnstile events on different calendar days', (t) => { - config.ACCESS_TOKEN = 'pk.*'; - const today = Date.now(); - const laterToday = today + 1; - const tomorrow = today + (25 * 60 * 60 * 1000); // Add a day - - const stub = t.stub(browser, 'now'); - stub.onCall(0).returns(today); - stub.onCall(1).returns(laterToday); - stub.onCall(2).returns(tomorrow); - - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); - mapbox.postTurnstileEvent(['b.tiles.mapbox.com']); - mapbox.postTurnstileEvent(['c.tiles.mapbox.com']); - - const reqToday = window.server.requests[0]; - reqToday.respond(200); - let reqBody = JSON.parse(reqToday.requestBody)[0]; - t.ok(reqBody.created, new Date(today).toISOString()); - - const reqTomorrow = window.server.requests[1]; - reqTomorrow.respond(200); - reqBody = JSON.parse(reqTomorrow.requestBody)[0]; - t.ok(reqBody.created, new Date(tomorrow).toISOString()); + t.test('when LocalStorage is not available', (t) => { + t.test('POSTs appuserTurnstile event', (t) => { + + config.ACCESS_TOKEN = 'pk.*'; + + mapbox.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.*`); + 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) => { + //Depend on having a successful POST in a prior test. + config.ACCESS_TOKEN = 'pk.*'; + + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + + const req = window.server.requests[0]; + t.false(req); + + t.end(); + }); + + t.test('POSTs appuserTurnstile event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + mapbox.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) => { + config.ACCESS_TOKEN = 'pk.*'; + const today = Date.now(); + const tomorrow = today + ms25Hours; // Add a day + t.stub(browser, 'now').callsFake(() => tomorrow); + + mapbox.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.*`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'appUserTurnstile'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + t.ok(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); + + t.test('Queues and POSTs appuserTurnstile events when triggerred in quick succession', (t) => { + config.ACCESS_TOKEN = 'pk.*'; + + let now = Date.now(); + t.stub(browser, 'now').callsFake(() => now); + + const today = now; + mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + + const laterToday = now + 1; + now = laterToday; + mapbox.postTurnstileEvent(['b.tiles.mapbox.com']); + + const tomorrow = laterToday + ms25Hours; // Add a day + now = tomorrow; + mapbox.postTurnstileEvent(['c.tiles.mapbox.com']); + + const reqToday = window.server.requests[0]; + reqToday.respond(200); + let reqBody = JSON.parse(reqToday.requestBody)[0]; + t.ok(reqBody.created, new Date(today).toISOString()); + + const reqTomorrow = window.server.requests[1]; + reqTomorrow.respond(200); + reqBody = JSON.parse(reqTomorrow.requestBody)[0]; + t.ok(reqBody.created, new Date(tomorrow).toISOString()); + + t.end(); + }); t.end(); }); From 749ec7aa7011df257f289eeab042f8fffca6ab58 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Mon, 30 Jul 2018 12:21:39 -0700 Subject: [PATCH 12/13] Expose TurnstileEvent class and fixup tests to be independent of order. --- src/util/mapbox.js | 24 +++--- test/unit/util/mapbox.test.js | 142 ++++++++++++++++++++++++---------- 2 files changed, 115 insertions(+), 51 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index dc1060f877d..bad2e266269 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -128,7 +128,7 @@ function formatUrl(obj: UrlObject): string { return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } -class TurnstileEvent { +export class TurnstileEvent { eventData: { anonId: ?string, lastSuccess: ?number, accessToken: ?string}; queue: Array; pending: boolean @@ -141,6 +141,16 @@ class TurnstileEvent { 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(); @@ -182,7 +192,7 @@ class TurnstileEvent { 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 < 0 || lastUpdate.getDate() !== nextDate.getDate(); + dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); } if (!dueForEvent) { @@ -227,12 +237,4 @@ class TurnstileEvent { const turnstileEvent_ = new TurnstileEvent(); -export const postTurnstileEvent = function (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); })) { - turnstileEvent_.queueRequest(browser.now()); - } -}; +export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 9b80e279a5f..11dddb10f6a 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -9,7 +9,11 @@ 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) => { @@ -274,10 +278,12 @@ test("mapbox", (t) => { t.end(); }); - t.test('.postTurnstileEvent', (t) => { + t.test('TurnstileEvent', (t) => { const ms25Hours = (25 * 60 * 60 * 1000); + let event; t.beforeEach((callback) => { window.useFakeXMLHttpRequest(); + event = new mapbox.TurnstileEvent(); callback(); }); @@ -286,19 +292,22 @@ test("mapbox", (t) => { 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; - mapbox.postTurnstileEvent([' a.tiles.mapxbox.com']); + 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) => { - config.ACCESS_TOKEN = 'pk.*'; - - mapbox.postTurnstileEvent(['a.tiles.boxmap.com']); + event.postTurnstileEvent(['a.tiles.boxmap.com']); t.equal(window.server.requests.length, 0); t.end(); @@ -329,7 +338,6 @@ test("mapbox", (t) => { }); t.test('does not POST event when previously stored data is on the same day', (t) => { - config.ACCESS_TOKEN = 'pk.*'; const now = +Date.now(); window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ @@ -339,14 +347,13 @@ test("mapbox", (t) => { t.stub(browser, 'now').callsFake(() => now + 5); // A bit later - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + 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) => { - config.ACCESS_TOKEN = 'pk.*'; const now = +Date.now(); window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ @@ -356,7 +363,7 @@ test("mapbox", (t) => { t.stub(browser, 'now').callsFake(() => now + ms25Hours); // next day - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); const req = window.server.requests[0]; req.respond(200); @@ -366,8 +373,7 @@ test("mapbox", (t) => { t.end(); }); - t.test('POSTs event when previously stored timestamp is in the future', (t) => { - config.ACCESS_TOKEN = 'pk.*'; + 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({ @@ -377,7 +383,7 @@ test("mapbox", (t) => { t.stub(browser, 'now').callsFake(() => now); // Past relative ot lastSuccess - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); const req = window.server.requests[0]; req.respond(200); @@ -388,14 +394,43 @@ test("mapbox", (t) => { }); t.test('does not POST appuserTurnstile event second time within same calendar day', (t) => { - config.ACCESS_TOKEN = 'pk.*'; + 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.stub(browser, 'now').callsFake(() => +Date.now() + (60 * 1000)); // A bit later + t.equal(window.server.requests.length, 1); - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + const reqBody = JSON.parse(req.requestBody)[0]; + t.ok(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]; - t.false(req); + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.ok(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -403,7 +438,7 @@ test("mapbox", (t) => { t.test('POSTs appuserTurnstile event when access token changes', (t) => { config.ACCESS_TOKEN = 'pk.new.*'; - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); const req = window.server.requests[0]; req.respond(200); @@ -418,16 +453,13 @@ test("mapbox", (t) => { t.test('when LocalStorage is not available', (t) => { t.test('POSTs appuserTurnstile event', (t) => { - - config.ACCESS_TOKEN = 'pk.*'; - - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + 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.*`); + 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); @@ -437,13 +469,43 @@ test("mapbox", (t) => { }); t.test('does not POST appuserTurnstile event second time within same calendar day', (t) => { - //Depend on having a successful POST in a prior test. - config.ACCESS_TOKEN = 'pk.*'; + 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.ok(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']); - mapbox.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]; - t.false(req); + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.ok(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -451,7 +513,7 @@ test("mapbox", (t) => { t.test('POSTs appuserTurnstile event when access token changes', (t) => { config.ACCESS_TOKEN = 'pk.new.*'; - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); const req = window.server.requests[0]; req.respond(200); @@ -466,19 +528,21 @@ test("mapbox", (t) => { t.end(); }); - t.test('POSTs appuserTurnstile event on next calendar day', (t) => { - config.ACCESS_TOKEN = 'pk.*'; - const today = Date.now(); - const tomorrow = today + ms25Hours; // Add a day - t.stub(browser, 'now').callsFake(() => tomorrow); + t.test('POSTs appUserTurnstile event on next calendar day', (t) => { + let now = +Date.now(); + t.stub(browser, 'now').callsFake(() => now); - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); + + now += ms25Hours; // Add a day + const tomorrow = now; + 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.*`); + 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); @@ -489,21 +553,19 @@ test("mapbox", (t) => { }); t.test('Queues and POSTs appuserTurnstile events when triggerred in quick succession', (t) => { - config.ACCESS_TOKEN = 'pk.*'; - let now = Date.now(); t.stub(browser, 'now').callsFake(() => now); const today = now; - mapbox.postTurnstileEvent(['a.tiles.mapbox.com']); + event.postTurnstileEvent(['a.tiles.mapbox.com']); const laterToday = now + 1; now = laterToday; - mapbox.postTurnstileEvent(['b.tiles.mapbox.com']); + event.postTurnstileEvent(['b.tiles.mapbox.com']); const tomorrow = laterToday + ms25Hours; // Add a day now = tomorrow; - mapbox.postTurnstileEvent(['c.tiles.mapbox.com']); + event.postTurnstileEvent(['c.tiles.mapbox.com']); const reqToday = window.server.requests[0]; reqToday.respond(200); From f92adfc3bae989d7effa0af899b0b5e6d22fb380 Mon Sep 17 00:00:00 2001 From: Asheem Mamoowala Date: Tue, 31 Jul 2018 15:46:17 -0700 Subject: [PATCH 13/13] Fix tests --- test/unit/util/mapbox.test.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 11dddb10f6a..b4ee9b4fa49 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -388,7 +388,7 @@ test("mapbox", (t) => { const req = window.server.requests[0]; req.respond(200); const reqBody = JSON.parse(req.requestBody)[0]; - t.ok(reqBody.created, new Date(now).toISOString()); + t.equal(reqBody.created, new Date(now).toISOString()); t.end(); }); @@ -409,7 +409,7 @@ test("mapbox", (t) => { t.equal(window.server.requests.length, 1); const reqBody = JSON.parse(req.requestBody)[0]; - t.ok(reqBody.created, new Date(firstEvent).toISOString()); + t.equal(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -430,7 +430,7 @@ test("mapbox", (t) => { t.equal(window.server.requests.length, 1); const reqBody = JSON.parse(req.requestBody)[0]; - t.ok(reqBody.created, new Date(firstEvent).toISOString()); + t.equal(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -484,7 +484,7 @@ test("mapbox", (t) => { t.equal(window.server.requests.length, 1); const reqBody = JSON.parse(req.requestBody)[0]; - t.ok(reqBody.created, new Date(firstEvent).toISOString()); + t.equal(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -505,7 +505,7 @@ test("mapbox", (t) => { t.equal(window.server.requests.length, 1); const reqBody = JSON.parse(req.requestBody)[0]; - t.ok(reqBody.created, new Date(firstEvent).toISOString()); + t.equal(reqBody.created, new Date(firstEvent).toISOString()); t.end(); }); @@ -538,16 +538,18 @@ test("mapbox", (t) => { const tomorrow = now; event.postTurnstileEvent(['a.tiles.mapbox.com']); - const req = window.server.requests[0]; + 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.ok(reqBody.created, new Date(tomorrow).toISOString()); + t.equal(reqBody.created, new Date(tomorrow).toISOString()); t.end(); }); @@ -570,12 +572,12 @@ test("mapbox", (t) => { const reqToday = window.server.requests[0]; reqToday.respond(200); let reqBody = JSON.parse(reqToday.requestBody)[0]; - t.ok(reqBody.created, new Date(today).toISOString()); + t.equal(reqBody.created, new Date(today).toISOString()); const reqTomorrow = window.server.requests[1]; reqTomorrow.respond(200); reqBody = JSON.parse(reqTomorrow.requestBody)[0]; - t.ok(reqBody.created, new Date(tomorrow).toISOString()); + t.equal(reqBody.created, new Date(tomorrow).toISOString()); t.end(); });