diff --git a/.gitignore b/.gitignore index a7cd074..660849d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -.idea/ -node_modules/ -bower_components/ -test/app/public/ -.eslintrc.js .editorconfig +.eslintrc.js + +/.idea/ +/bower_components/ +/node_modules/ +/test/app/public/ diff --git a/Makefile b/Makefile index 32a8b15..8b2520c 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,6 @@ include n.Makefile -test: verify - karma start +unit-test: + karma start test/karma.conf.js -test-app: - rm -rf test/app/public/ - mkdir test/app/public/ - browserify test/app/main.js --debug --transform debowerify > test/app/public/bundle.js - node test/app/server.js +test: verify unit-test diff --git a/bower.json b/bower.json index 873675b..3ae7e48 100644 --- a/bower.json +++ b/bower.json @@ -12,9 +12,5 @@ "bower_components", "test", "tests" - ], - "devDpendencies": { - "fetch": "github/fetch#^0.9.0", - "es6-promise": "^2.1.0" - } + ] } diff --git a/circle.yml b/circle.yml index d6eaee7..8e7b29b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,9 +1,3 @@ machine: node: version: 4.4.7 -deployment: - release: - tag: /^v[0-9]+(.[0-9]+)*/ - owner: Financial-Times - commands: - - make npm-publish diff --git a/main.js b/main.js index ecb761b..644e794 100644 --- a/main.js +++ b/main.js @@ -1,55 +1,78 @@ -'use strict'; -const request = require('./src/request'); -const cache = require('./src/cache'); -const promises = {}; - -function uuid (){ - const uuid = cache('uuid') - - if (uuid){ - return Promise.resolve({ - uuid:uuid - }); +import request from './src/request'; +import cache from './src/cache'; + +const requests = {}; + +// DEPRECATED: use the secure session ID, via getSessionId +const getCookie = () => { + return (/FTSession=([^;]+)/.exec(document.cookie) || [null, ''])[1]; +}; + +const getSessionId = () => { + const [, sessionId] = /FTSession_s=([^;]+)/.exec(document.cookie) || []; + return sessionId; +}; + +const getUuid = () => { + const cachedUUID = cache('uuid'); + if (cachedUUID) { + return Promise.resolve({ uuid: cachedUUID }); } - if (!promises.uuid) { - promises.uuid = request('/uuid').then(function (response){ - cache('uuid', response.uuid); - return response; - }); + + const sessionId = getSessionId(); + if (!sessionId) { + return Promise.resolve({ uuid: undefined }); } - return promises.uuid; -} + if (!requests.uuid) { + requests.uuid = request(`/sessions/s/${sessionId}`) + .then(({ uuid } = {}) => { + delete requests.uuid; + if (uuid) { + cache('uuid', uuid); + } + return { uuid }; + }); + } + + return requests.uuid; +}; -function products () { +const getProducts = () => { const cachedProducts = cache('products'); const cachedUUID = cache('uuid'); - - if(cachedProducts && cachedUUID){ - return Promise.resolve({products:cachedProducts, uuid:cachedUUID}); + if (cachedProducts && cachedUUID){ + return Promise.resolve({ products: cachedProducts, uuid: cachedUUID }); } - if (!promises.products) { - promises.products = request('/products').then(function (response) { - cache('products', response.products); - cache('uuid', response.uuid); - return response; - }); + if (!requests.products) { + requests.products = request('/products', { credentials: 'include' }) + .then(({ products, uuid } = {}) => { + delete requests.products; + if (products) { + cache('uuid', uuid); + } + if (uuid) { + cache('uuid', uuid); + } + return { products, uuid }; + }); } - return promises.products; -} + return requests.products; +}; -function validate () { - return request('/validate'); -} +// DEPRECATED: use getUuid, will only return a uuid if session is valid +const validate = () => { + return getUuid() + .then(({ uuid }) => uuid ? true : false); +}; -module.exports = { - uuid : uuid, - validate : validate, - cache : cache, - products: products, - cookie : function () { - return (/FTSession=([^;]+)/.exec(document.cookie) || [null, ''])[1]; - } +export default { + uuid: getUuid, + products: getProducts, + validate, + cache, + cookie: getCookie, + sessionId: getSessionId }; diff --git a/n.Makefile b/n.Makefile index e23f86d..97d7e8b 100644 --- a/n.Makefile +++ b/n.Makefile @@ -14,6 +14,8 @@ endif # Enforce repo ownership ifeq ("$(wildcard ft.yml)","") $(error 'Projects making use of n-makefile *must* define an ft.yml file containing the repo owner's details (see any next- repo for required structure)') +$(error 'If you are creating a project not to be maintained by the next team please feel free to copy what you need from our build tools but don't add an ft.yml.') +$(error 'Integrating with our tooling may result in unwanted effects e.g. nightly builds, slack alerts, emails etc') endif # ./node_modules/.bin on the PATH @@ -28,7 +30,7 @@ NPM_INSTALL = npm prune --production=false && npm install BOWER_INSTALL = bower prune && bower install --config.registry.search=http://registry.origami.ft.com --config.registry.search=https://bower.herokuapp.com JSON_GET_VALUE = grep $1 | head -n 1 | sed 's/[," ]//g' | cut -d : -f 2 IS_GIT_IGNORED = grep -q $(if $1, $1, $@) .gitignore -VERSION = v14 +VERSION = v15 APP_NAME = $(shell cat package.json 2>/dev/null | $(call JSON_GET_VALUE,name)) DONE = echo ✓ $@ done CONFIG_VARS = curl -fsL https://ft-next-config-vars.herokuapp.com/$1/$(call APP_NAME)$(if $2,.$2,) -H "Authorization: `heroku config:get APIKEY --app ft-next-config-vars`" diff --git a/package.json b/package.json index 3a24a2f..6c074ea 100644 --- a/package.json +++ b/package.json @@ -20,23 +20,25 @@ }, "homepage": "https://github.com/Financial-Times/next-session-component", "devDependencies": { + "babel-loader": "^6.4.0", + "babel-preset-es2015": "^6.24.0", "bower": "^1.7.9", - "browserify": "^10.1.3", - "chai": "^2.3.0", - "debowerify": "^1.2.1", - "eslint": "^3.0.1", + "chai": "^3.0.0", + "eslint": "^3.16.1", "express": "^4.12.3", - "fetch-mock": "^1.4.1", - "isomorphic-fetch": "^2.0.2", - "karma": "^0.12.31", - "karma-browserify": "^4.1.2", - "karma-chrome-launcher": "^0.1.10", - "karma-mocha": "^0.1.10", - "karma-phantomjs-launcher": "^0.1.4", - "lintspaces-cli": "^0.4.0", - "mocha": "^2.2.4", - "npm-prepublish": "^1.2.1", - "origami-build-tools": "^2.13.0", - "sinon": "^1.14.1" + "karma": "^1.5.0", + "karma-chai": "^0.1.0", + "karma-cli": "^1.0.1", + "karma-firefox-launcher": "^1.0.0", + "karma-mocha": "^1.3.0", + "karma-sinon": "^1.0.5", + "karma-webpack": "^2.0.3", + "lintspaces-cli": "^0.6.0", + "mocha": "^3.2.0", + "sinon": "^1.14.1", + "webpack": "^2.2.1" + }, + "engines": { + "node": "4.4.7" } } diff --git a/src/cache.js b/src/cache.js index 86bb274..f24e20a 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,32 +1,29 @@ -'use strict'; - let detailsCache = {}; - -function cache (name, value) { - if(typeof name === 'object'){ +const cache = (name, value) => { + if (typeof name === 'object'){ detailsCache = name; return; } - if(typeof name === 'string' && typeof value === 'string') { + if (typeof name === 'string' && typeof value === 'string') { detailsCache[name] = value; return; } - if(typeof name === 'string' && typeof value === 'undefined') { + if (typeof name === 'string' && typeof value === 'undefined') { return detailsCache[name] || null; } - if(typeof name === 'undefined' && typeof value === 'undefined') { + if (typeof name === 'undefined' && typeof value === 'undefined') { return detailsCache; } throw new Error('Invalid arguments'); } -cache.clear = function () { +cache.clear = () => { detailsCache = {}; }; -module.exports = cache; +export default cache; diff --git a/src/request.js b/src/request.js index be6b52b..45a3a95 100644 --- a/src/request.js +++ b/src/request.js @@ -1,38 +1,27 @@ -'use strict'; - -function request (url) { - // if we don't have a session token cookie, don't bother.. - if (document.cookie.indexOf('FTSession=') === -1) { - return Promise.reject(new Error('No session cookie found')); - } - +export default (url, { credentials = 'omit' } = {}) => { return fetch(`https://session-next.ft.com${url}`, { - credentials:'include', + credentials, useCorsProxy: true - }).then(function (response) { - if (response.ok){ - return (url === '/validate') ? Promise.resolve(true) : response.json(); - } else { - return (url === '/validate') ? Promise.resolve(false) : Promise.reject(response.status); - } - - }).catch(function (e) { - const message = e.message || ''; - if (message.indexOf('timed out') > -1 || message.indexOf('Network request failed') > -1 || message.indexOf('Not Found') > -1) { - // HTTP timeouts and invalid sessions are a fact of life on the internet. - // We don't want to report this to Sentry. - } else { + }) + .then(response => { + if (response.ok){ + return response.json(); + } else { + return response.text() + .then(text => { + throw new Error(`Next session responded with "${text}" (${response.status})`); + }); + } + }) + .catch(err => { document.body.dispatchEvent(new CustomEvent('oErrors.log', { bubbles: true, detail: { - error: e, + error: err, info: { component: 'next-session-client' } } - })) - } - }); + })); + }); } - -module.exports = request; diff --git a/test/app/index.html b/test/app/index.html deleted file mode 100644 index b2e6c03..0000000 --- a/test/app/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - APP TEST - - - - -
- - - - -
- -
- -
- - - diff --git a/test/app/main.js b/test/app/main.js deleted file mode 100644 index 77b221a..0000000 --- a/test/app/main.js +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable no-console */ -'use strict'; - -require('fetch'); -require('es6-promise').polyfill(); - -const session = require('../../main'); - -function handleClick (e){ - const target = e.target; - const method = target.getAttribute('data-method'); - console.log('Call method ' + method); - const result = session[method](); - if(result.then){ - result.then(function (response){ - console.log('Response from ' + method + ' is:'); - console.dir(response); - }).catch(function (err){ - console.error(err); - }); - } else { - console.log('Response from ' + method + ' is:'); - console.dir(result); - } - -} - -function addClick (el){ - console.log('add click to', el); - el.addEventListener('click', handleClick); -} - -[].slice.call(document.querySelectorAll('.buttons button'), 0).forEach(addClick); diff --git a/test/app/server.js b/test/app/server.js deleted file mode 100644 index f7cabae..0000000 --- a/test/app/server.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable no-console */ -'use strict'; - -const express = require('express'); -const app = express(); -const PORT = 5656; -const path = require('path'); - -app.get('/', function (req, res){ - app.use(express.static(path.resolve(__dirname, 'public'))); - res.sendFile(path.resolve(__dirname, './index.html')); -}); - -app.listen(PORT); - -console.log('listening on %s', PORT); diff --git a/test/cache.spec.js b/test/cache.spec.js deleted file mode 100644 index 24c669a..0000000 --- a/test/cache.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; -/*global describe, it*/ -const cache = require('../src/cache'); -const expect = require('chai').expect; - - -describe('Cache', function (){ - - it('Should be able to save and retrieve an object', function (done){ - const obj = {foo:'bar'}; - cache(obj); - expect(cache()).to.deep.equal(obj); - done(); - }); - - it('Should be able to save and retrieve a saved value', function (done){ - const uuid = 'svsdvsdvsdvsv'; - cache('uuid', uuid); - expect(cache('uuid')).to.equal(uuid); - done(); - }); - -}); diff --git a/karma.conf.js b/test/karma.conf.js similarity index 69% rename from karma.conf.js rename to test/karma.conf.js index 8e7acf8..e6a1b58 100644 --- a/karma.conf.js +++ b/test/karma.conf.js @@ -1,9 +1,8 @@ -'use strict'; // Karma configuration -// Generated on Tue May 12 2015 10:54:45 GMT+0100 (BST) +// Generated on Thu May 28 2015 16:29:05 GMT+0100 (BST) module.exports = function (config) { - const configuration = { + config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', @@ -11,12 +10,12 @@ module.exports = function (config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['browserify', 'mocha'], + frameworks: ['mocha', 'chai', 'sinon'], // list of files / patterns to load in the browser files: [ - 'test/*.spec.js' + '**/*.spec.js' ], @@ -28,12 +27,21 @@ module.exports = function (config) { // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - 'test/*.js': [ 'browserify' ] + '**/*.js': [ 'webpack' ] }, - browserify: { - transform: ['debowerify'], - debug: true + + webpack: { + module: { + loaders: [{ + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel-loader', + query: { + presets: ['es2015'] + } + }] + } }, // test results reporter to use @@ -52,34 +60,20 @@ module.exports = function (config) { // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_DEBUG, + logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, + autoWatch: true, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome'], + browsers: ['Firefox'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits - singleRun: true, - - customLaunchers: { - Chrome_travis_ci: { - base: 'Chrome', - flags: ['--no-sandbox'] - } - } - }; - - if(process.env.TRAVIS){ - configuration.singleRun = true; - configuration.browsers = ['PhantomJS']; - } - - config.set(configuration); + singleRun: true + }); }; diff --git a/test/main.spec.js b/test/main.spec.js index 2022f0b..4ee6475 100644 --- a/test/main.spec.js +++ b/test/main.spec.js @@ -1,13 +1,7 @@ -/* eslint-disable no-console */ -'use strict'; -/*global describe, it, before, afterEach*/ - -const session = require('../main'); -const sinon = require('sinon'); -const expect = require('chai').expect; +/* global sinon */ +import session from '../main'; describe('Session Client', function () { - const jsonpCallbackName = '$$$JSONP_CALLBACK'; const sessionData = {'uuid':'e1d24b36-30de-434d-a087-e04c17377ac7', 'products': 'P0, P1'}; @@ -48,28 +42,29 @@ describe('Session Client', function () { setupJsonp(data, success); } - it('Should be able to get session uuid', function (done){ - setup({uuid:sessionData.uuid}, true); - session.uuid().then(function (response){ - expect(response.uuid).to.equal(sessionData.uuid); - done(); - }).catch(done); + it('Should be able to get session uuid', function () { + window.document.cookie = 'FTSession_s=1234567890'; + setup({ uuid: sessionData.uuid }, true); + return session.uuid() + .then(function (response) { + response.uuid.should.equal(sessionData.uuid); + }); }); - it('Should be able to check if a session is valid', function (done){ + it('Should be able to check if a session is valid', function () { setup({}, false); - session.validate().then(function (isValid){ - expect(isValid).to.be.false; - done(); - }).catch(done); + return session.validate() + .then(function (isValid) { + isValid.should.be.false; + }); }); it('Should be able to get the user\'s products', function () { setup({uuid:sessionData.uuid, products:sessionData.products}, true); - return session.products().then(function (response) { - console.log(response); - expect(response.products).to.equal(sessionData.products); - }); + return session.products() + .then(function (response) { + response.products.should.equal(sessionData.products); + }); }); }); @@ -81,7 +76,7 @@ describe('Cookie helper', function () { document.cookie = c + ';'; }); - expect(session.cookie()).to.equal('hs9uy(S89`uxf9mymwapSDSA*&Ofnyszyo'); + session.cookie().should.equal('hs9uy(S89`uxf9mymwapSDSA*&Ofnyszyo'); }); it('should extract missing session id from document.cookie', function () { @@ -89,6 +84,6 @@ describe('Cookie helper', function () { .split(';').forEach(function (c) { document.cookie = c + ';'; }); - expect(session.cookie()).to.equal(''); + session.cookie().should.equal(''); }); }); diff --git a/test/src/cache.spec.js b/test/src/cache.spec.js new file mode 100644 index 0000000..666efe4 --- /dev/null +++ b/test/src/cache.spec.js @@ -0,0 +1,17 @@ +import cache from '../../src/cache'; + +describe('Cache', function () { + + it('Should be able to save and retrieve an object', function () { + const obj = { foo: 'bar' }; + cache(obj); + cache().should.eql(obj); + }); + + it('Should be able to save and retrieve a saved value', function () { + const uuid = 'svsdvsdvsdvsv'; + cache('uuid', uuid); + cache('uuid').should.equal(uuid); + }); + +}); diff --git a/test/request.spec.js b/test/src/request.spec.js similarity index 71% rename from test/request.spec.js rename to test/src/request.spec.js index 785b462..d91e664 100644 --- a/test/request.spec.js +++ b/test/src/request.spec.js @@ -1,12 +1,7 @@ -'use strict'; -/*global describe, it, before, afterEach*/ - -const sinon = require('sinon'); -const request = require('../src/request'); +/* global sinon */ +import request from '../../src/request'; describe('request', function () { - - before(function () { if(document.cookie.indexOf('FTSession=') < 0){ document.cookie += 'FTSession=0-HSSzYw3kNN06CH4EwXN3rHzwAAAU1NL7xtww.MEYCIQDXxFJypS8uRn86Fjlcw9wVrf2vzC2kkd9XbcJmZpenrwIhAP0q2fTmfBa0WkJzGtnrwfcuIl3oBxXzwabrcHXE41xi'; @@ -27,16 +22,14 @@ describe('request', function () { window.fetch = sinon.stub().returns(Promise.resolve({ok:true, json:jsonFunc})); } - it('Should be able to make a CORS request to the session service', function (done){ + it('Should be able to make a CORS request to the session service', function () { const email = 'paul.i.wilson@ft.com'; setupFetch({email:email}); - request('/').then(function () { - try{ - sinon.assert.calledWith(window.fetch, 'https://session-next.ft.com/', {credentials:'include', useCorsProxy: true}); - }catch(e){ - done(e); - } - done(); - }).catch(done); + return request('/') + .then(function () { + sinon.assert.calledWith(window.fetch, 'https://session-next.ft.com/', { + credentials: 'omit', useCorsProxy: true + }); + }); }); });