From a07d4dad128f71e4c47fa92c1e25a4f0ba6467f4 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 12 Nov 2015 15:28:43 +0100 Subject: [PATCH 01/35] port i18n lib to es6 and extract middleware --- website/src/libs/api-v3/i18n.js | 98 +++++++++++++++++++ .../src/middlewares/api-v3/getUserLanguage.js | 85 ++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 website/src/libs/api-v3/i18n.js create mode 100644 website/src/middlewares/api-v3/getUserLanguage.js diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js new file mode 100644 index 0000000000..96ad15a2e7 --- /dev/null +++ b/website/src/libs/api-v3/i18n.js @@ -0,0 +1,98 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; +import shared from '../../../../common'; + +const localePath = path.join(__dirname, '/../../../../common/locales/'); + +// Store translations +export let translations = {}; +// Store MomentJS localization files +export let momentLangs = {}; + +// Handle differencies in language codes between MomentJS and /locales +let momentLangsMapping = { + en: 'en-gb', + en_GB: 'en-gb', // eslint-disable-line camelcase + no: 'nn', + zh: 'zh-cn', + es_419: 'es', // eslint-disable-line camelcase +}; + +function _loadTranslations (locale) { + let files = fs.readdirSync(path.join(localePath, locale)); + + translations[locale] = {}; + + files.forEach((file) => { + if (path.extname(file) !== '.json') return; + + // We use require to load and parse a JSON file + _.merge(translations[locale], require(path.join(localePath, locale, file))); // eslint-disable-line global-require + }); +} + +// First fetch English strings so we can merge them with missing strings in other languages +_loadTranslations('en'); + +// Then load all other languages +fs.readdirSync(localePath).forEach((file) => { + if (file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return; + _loadTranslations(file); + + // Merge missing strings from english + _.defaults(translations[file], translations.en); +}); + +// Add translations to shared +shared.i18n.translations = translations; + +export let langCodes = Object.keys(translations); + +export let avalaibleLanguages = langCodes.map((langCode) => { + return { + code: langCode, + name: translations[langCode].languageName, + }; +}); + +langCodes.forEach((code) => { + let lang = _.find(avalaibleLanguages, {code}); + + lang.momentLangCode = momentLangsMapping[code] || code; + + try { + // MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files + // We wrap everything in a try catch because the file might not exist + let f = fs.readFileSync(path.join(__dirname, `/../../../node_modules/moment/locale/${lang.momentLangCode}.js`), 'utf8'); + + momentLangs[code] = f; + } catch (e) { // eslint-disable-lint no-empty + // TODO implement some type of error loggin? + // The catch block is mandatory so can't be removed + } +}); + +// Remove en_GB from langCodes checked by browser to avoid it being +// used in place of plain original 'en' (it's an optional language that can be enabled only in setting) +export let defaultLangCodes = _.without(langCodes, 'en_GB'); + +// A map of languages that have different versions and the relative versions +export let multipleVersionsLanguages = { + es: ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe', + 'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn', + 'es-ni', 'es-pr'], + zh: ['zh-tw'], +}; + +// Export en strings only, temporary solution for mobile +// This is copied from middlewares/locals#t() +// TODO review if this can be removed since the old mobile app is no longer active +// stringName and vars are the allowed parameters +export function enTranslations (...args) { + let language = _.find(avalaibleLanguages, {code: 'en'}); + + // language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined); + args.push(language.code); + return shared.i18n.t(...args); +} \ No newline at end of file diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js new file mode 100644 index 0000000000..9a273b79bf --- /dev/null +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -0,0 +1,85 @@ +import { model as User } from '../../models/user'; +import accepts from 'accepts'; +import _ from 'lodash'; +import { + translations, + defaultLangCodes, + multipleVersionsLanguages, +} from '../../libs/api-v3/i18n'; + +function _getFromBrowser (req) { + let acceptedLanguages = accepts(req).languages(); + + let acceptable = _(acceptedLanguages).map((lang) => { + return lang.slice(0, 2); + }).uniq().value(); + + let matches = _.intersection(acceptable, defaultLangCodes); + + let iAcceptedCompleteLang = matches.length > 0 ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1; + + if (iAcceptedCompleteLang !== -1) { + let acceptedCompleteLang = _.find(acceptedLanguages, (accepted) => { + return accepted.slice(0, 2) === multipleVersionsLanguages[iAcceptedCompleteLang]; + }); + + if (acceptedCompleteLang) { + acceptedCompleteLang = acceptedCompleteLang.toLowerCase(); + } else { + return 'en'; + } + + if (matches[0] === 'es') { + // In case of a Latin American version of Spanish use 'es_419' + return multipleVersionsLanguages.es.indexOf(acceptedCompleteLang !== -1) ? 'es_419' : 'es'; + } else if (matches[0] === 'zh') { + let iChinese = multipleVersionsLanguages.zh.indexOf(acceptedCompleteLang.toLowerCase()); + + return iChinese !== -1 ? multipleVersionsLanguages.zh[iChinese] : 'zh'; + } else { + return 'en'; + } + } else if (matches.length > 0) { + return matches[0].toLowerCase(); + } else { + return 'en'; + } +} + +function _getFromUser (user, req) { + let lang; + + if (user && user.preferences.language && translations[user.preferences.language]) { + lang = user.preferences.language; + } else { + let preferred = _getFromBrowser(req); + + lang = translations[preferred] ? preferred : 'en'; + } + + return lang; +} + +export default function getUserLanguage (req, res, next) { + if (req.query.lang) { // In case the language is specified in the request url, use it + req.language = translations[req.query.lang] ? req.query.lang : 'en'; + return next(); + } else if (req.locals && req.locals.user) { // If the request is authenticated, use the user's preferred language + req.language = _getFromUser(req.locals.user, req); + return next(); + } else if (req.session && req.session.userId) { // Same thing if the user has a valid session + User + .findOne({ + _id: req.session.userId, + }, 'preferences.language') + .exec() + .then((user) => { + req.language = _getFromUser(user, req); + return next(); + }) + .catch(next); + } else { // Otherwise get from browser + req.language = _getFromUser(null, req); + return next(); + } +} \ No newline at end of file From 4af8a8f7aae815987b881d6b53961f629878e317 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 12 Nov 2015 16:24:08 +0100 Subject: [PATCH 02/35] add tests for i18n and getUserLanguage --- test/api/v3/unit/libs/i18n.test.js | 38 ++++++++ .../unit/middlewares/getUserLanguage.test.js | 89 +++++++++++++++++++ website/src/libs/api-v3/i18n.js | 2 +- .../src/middlewares/api-v3/getUserLanguage.js | 1 + 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 test/api/v3/unit/libs/i18n.test.js create mode 100644 test/api/v3/unit/middlewares/getUserLanguage.test.js diff --git a/test/api/v3/unit/libs/i18n.test.js b/test/api/v3/unit/libs/i18n.test.js new file mode 100644 index 0000000000..8acef06486 --- /dev/null +++ b/test/api/v3/unit/libs/i18n.test.js @@ -0,0 +1,38 @@ +import { + translations, + localePath, + langCodes, +} from '../../../../../website/src/libs/api-v3/i18n'; +import fs from 'fs'; +import path from 'path'; + +describe('i18n', () => { + describe('translations', () => { + it('loads all locales', (done) => { + fs.readdir(localePath, (err, files) => { + if (err) return done(err); + let locales = []; + + files.forEach((file) => { + if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return; + locales.push(file); + }); + + locales = locales.sort(); + let loaded = Object.keys(translations).sort(); + + expect(locales).to.eql(loaded); + done(); + }); + }); + + it('keeps a list all locales', () => { + expect(Object.keys(translations).sort()).to.eql(langCodes.sort()); + }); + + it('has an english translations', () => { + expect(langCodes).to.contain('en'); + expect(translations.en).to.be.an('object'); + }); + }); +}); diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js new file mode 100644 index 0000000000..4a9bb0ec04 --- /dev/null +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -0,0 +1,89 @@ +import { + generateRes, + generateReq, + generateNext, +} from '../../../../helpers/api-unit.helper'; +import getUserLanguage from '../../../../../website/src/middlewares/api-v3/getUserLanguage'; +import Q from 'q'; +import { model as User } from '../../../../../website/src/models/user'; +import { translations } from '../../../../../website/src/libs/api-v3/i18n'; +import accepts from 'accepts'; + +describe('getUserLanguage', () => { + let res, req, next; + + beforeEach(() => { + res = generateRes(); + req = generateReq(); + next = generateNext(); + + sandbox.stub(User, 'findOne').returns({ + exec() { + return Q.resolve({ + preferences: { + language: 'it', + } + }); + } + }); + }); + + describe('query parameter', () => { + it('uses the language in the query parameter if avalaible', () => { + req.query = { + lang: 'es', + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('es'); + }); + + it('falls back to english if the query parameter language does not exists', () => { + req.query = { + lang: 'bla', + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('en'); + }); + }); + + describe('authorized request', () => { + it('uses the user preferred language if avalaible', () => { + req.locals = { + user: { + preferences: { + language: 'it', + }, + }, + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('it'); + }); + + xit('falls back to english if the user preferred language is not avalaible', () => { + req.locals = { + user: { + preferences: { + language: 'bla', + }, + }, + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('en'); + }); + }); + + describe('request with session', () => { + it('uses the user preferred language if avalaible', () => { + req.session = { + userId: 123 + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('it'); + }); + }); +}); diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js index 96ad15a2e7..e8a1b630cc 100644 --- a/website/src/libs/api-v3/i18n.js +++ b/website/src/libs/api-v3/i18n.js @@ -3,7 +3,7 @@ import path from 'path'; import _ from 'lodash'; import shared from '../../../../common'; -const localePath = path.join(__dirname, '/../../../../common/locales/'); +export const localePath = path.join(__dirname, '/../../../../common/locales/'); // Store translations export let translations = {}; diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 9a273b79bf..813fe848fc 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -75,6 +75,7 @@ export default function getUserLanguage (req, res, next) { .exec() .then((user) => { req.language = _getFromUser(user, req); + console.log(req.language); return next(); }) .catch(next); From de21b72027403dc70e6a75de952d9f1164673fe7 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 12 Nov 2015 16:43:02 +0100 Subject: [PATCH 03/35] move old i18n version to api-v2 folder and use new i18n where possible --- test/common/algos.mocha.coffee | 2 +- test/common/dailies.coffee | 2 +- test/common/user.fns.ultimateGear.test.js | 2 +- test/helpers/api-integration.helper.js | 2 +- test/helpers/api-unit.helper.js | 2 +- test/helpers/content.helper.js | 2 +- website/src/controllers/api-v2/auth.js | 2 +- website/src/libs/api-v2/analytics.js | 2 +- website/src/libs/{ => api-v2}/i18n.js | 0 website/src/libs/api-v3/analyticsService.js | 1 - website/src/middlewares/locals.js | 2 +- website/src/routes/api-v2/auth.js | 2 +- website/src/routes/api-v2/coupon.js | 2 +- website/src/routes/api-v2/swagger.js | 2 +- website/src/routes/api-v2/unsubscription.js | 2 +- website/src/routes/dataexport.js | 2 +- website/src/routes/pages.js | 2 +- website/src/routes/payments.js | 2 +- website/src/server.js | 2 +- 19 files changed, 17 insertions(+), 18 deletions(-) rename website/src/libs/{ => api-v2}/i18n.js (100%) diff --git a/test/common/algos.mocha.coffee b/test/common/algos.mocha.coffee index b413c3e000..cec964a8bd 100644 --- a/test/common/algos.mocha.coffee +++ b/test/common/algos.mocha.coffee @@ -3,7 +3,7 @@ expect = require 'expect.js' sinon = require 'sinon' moment = require 'moment' shared = require '../../common/script/index.coffee' -shared.i18n.translations = require('../../website/src/libs/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations test_helper = require './test_helper' test_helper.addCustomMatchers() $w = (s)->s.split(' ') diff --git a/test/common/dailies.coffee b/test/common/dailies.coffee index e5e3fad81a..869583d37b 100644 --- a/test/common/dailies.coffee +++ b/test/common/dailies.coffee @@ -3,7 +3,7 @@ expect = require 'expect.js' sinon = require 'sinon' moment = require 'moment' shared = require '../../common/script/index.coffee' -shared.i18n.translations = require('../../website/src/libs/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations repeatWithoutLastWeekday = ()-> repeat = {su:true,m:true,t:true,w:true,th:true,f:true,s:true} diff --git a/test/common/user.fns.ultimateGear.test.js b/test/common/user.fns.ultimateGear.test.js index d2b82f43e8..33d3c4f52c 100644 --- a/test/common/user.fns.ultimateGear.test.js +++ b/test/common/user.fns.ultimateGear.test.js @@ -1,7 +1,7 @@ 'use strict'; var shared = require('../../common/script/index.coffee'); -shared.i18n.translations = require('../../website/src/libs/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations require('./test_helper'); diff --git a/test/helpers/api-integration.helper.js b/test/helpers/api-integration.helper.js index 1ddaa75b0d..6e78046f8e 100644 --- a/test/helpers/api-integration.helper.js +++ b/test/helpers/api-integration.helper.js @@ -9,7 +9,7 @@ import {v4 as generateUUID} from 'uuid'; import superagent from 'superagent'; import i18n from '../../common/script/src/i18n'; require('coffee-script'); -i18n.translations = require('../../website/src/libs/i18n.js').translations; +i18n.translations = require('../../website/src/libs/api-v3/i18n').translations; const API_TEST_SERVER_PORT = 3003; diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index 95a34316e1..2e789d53a9 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -3,7 +3,7 @@ import { model as User } from '../../website/src/models/user' import { model as Group } from '../../website/src/models/group' import i18n from '../../common/script/src/i18n'; require('coffee-script'); -i18n.translations = require('../../website/src/libs/i18n.js').translations; +i18n.translations = require('../../website/src/libs/api-v3/i18n.js').translations; afterEach(() => { sandbox.restore(); diff --git a/test/helpers/content.helper.js b/test/helpers/content.helper.js index c54a62d508..2f885f0830 100644 --- a/test/helpers/content.helper.js +++ b/test/helpers/content.helper.js @@ -3,7 +3,7 @@ import {each} from 'lodash'; import i18n from '../../common/script/src/i18n'; require('coffee-script'); -i18n.translations = require('../../website/src/libs/i18n.js').translations; +i18n.translations = require('../../website/src/libs/api-v3/i18n').translations; export const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.'; export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/; diff --git a/website/src/controllers/api-v2/auth.js b/website/src/controllers/api-v2/auth.js index b01e1aba3d..524615760e 100644 --- a/website/src/controllers/api-v2/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -10,7 +10,7 @@ var FirebaseTokenGenerator = require('firebase-token-generator'); var User = require('../../models/user').model; var EmailUnsubscription = require('../../models/emailUnsubscription').model; var analytics = utils.analytics; -var i18n = require('./../../libs/i18n'); +var i18n = require('./../../libs/api-v2/i18n'); var isProd = nconf.get('NODE_ENV') === 'production'; diff --git a/website/src/libs/api-v2/analytics.js b/website/src/libs/api-v2/analytics.js index 315dc4b618..f7c1391a11 100644 --- a/website/src/libs/api-v2/analytics.js +++ b/website/src/libs/api-v2/analytics.js @@ -1,5 +1,5 @@ require('coffee-script'); -require('./i18n'); +require('./api-v2/i18n'); var _ = require('lodash'); var Content = require('../../../common').content; diff --git a/website/src/libs/i18n.js b/website/src/libs/api-v2/i18n.js similarity index 100% rename from website/src/libs/i18n.js rename to website/src/libs/api-v2/i18n.js diff --git a/website/src/libs/api-v3/analyticsService.js b/website/src/libs/api-v3/analyticsService.js index 6e460097eb..81be37322e 100644 --- a/website/src/libs/api-v3/analyticsService.js +++ b/website/src/libs/api-v3/analyticsService.js @@ -10,7 +10,6 @@ import { import { content as Content } from '../../../../common'; require('coffee-script'); -require('../../libs/i18n'); const AMPLIUDE_TOKEN = nconf.get('AMPLITUDE_KEY'); const GA_TOKEN = nconf.get('GA_ID'); diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index 2f238a6a99..e6b2fa8818 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -2,7 +2,7 @@ var nconf = require('nconf'); var _ = require('lodash'); var utils = require('../libs/utils'); var shared = require('../../../common'); -var i18n = require('../libs/i18n'); +var i18n = require('../libs/api-v2/i18n'); var buildManifest = require('../libs/buildManifest'); var shared = require('../../../common'); var forceRefresh = require('./forceRefresh'); diff --git a/website/src/routes/api-v2/auth.js b/website/src/routes/api-v2/auth.js index c60f44547b..d76891e40d 100644 --- a/website/src/routes/api-v2/auth.js +++ b/website/src/routes/api-v2/auth.js @@ -1,6 +1,6 @@ var auth = require('../../controllers/api-v2/auth'); var express = require('express'); -var i18n = require('../../libs/i18n'); +var i18n = require('../../libs/api-v2/i18n'); var router = new express.Router(); /* auth.auth*/ diff --git a/website/src/routes/api-v2/coupon.js b/website/src/routes/api-v2/coupon.js index 811d81a6f2..132184a585 100644 --- a/website/src/routes/api-v2/coupon.js +++ b/website/src/routes/api-v2/coupon.js @@ -3,7 +3,7 @@ var express = require('express'); var router = new express.Router(); var auth = require('../../controllers/api-v2/auth'); var coupon = require('../../controllers/api-v2/coupon'); -var i18n = require('../../libs/i18n'); +var i18n = require('../../libs/api-v2/i18n'); router.get('/api/v2/coupons', auth.authWithUrl, i18n.getUserLanguage, coupon.ensureAdmin, coupon.getCoupons); router.post('/api/v2/coupons/generate/:event', auth.auth, i18n.getUserLanguage, coupon.ensureAdmin, coupon.generateCoupons); diff --git a/website/src/routes/api-v2/swagger.js b/website/src/routes/api-v2/swagger.js index d93cfe6271..94bac8ad7b 100644 --- a/website/src/routes/api-v2/swagger.js +++ b/website/src/routes/api-v2/swagger.js @@ -20,7 +20,7 @@ var nconf = require("nconf"); var cron = user.cron; var _ = require('lodash'); var content = require('../../../../common').content; -var i18n = require('../../libs/i18n'); +var i18n = require('../../libs/api-v2/i18n'); var forceRefresh = require('../../middlewares/forceRefresh').middleware; module.exports = function(swagger, v2) { diff --git a/website/src/routes/api-v2/unsubscription.js b/website/src/routes/api-v2/unsubscription.js index 942a396eef..3b31305b5a 100644 --- a/website/src/routes/api-v2/unsubscription.js +++ b/website/src/routes/api-v2/unsubscription.js @@ -1,6 +1,6 @@ var express = require('express'); var router = new express.Router(); -var i18n = require('../../libs/i18n'); +var i18n = require('../../libs/api-v2/i18n'); var unsubscription = require('../../controllers/api-v2/unsubscription'); router.get('/unsubscribe', i18n.getUserLanguage, unsubscription.unsubscribe); diff --git a/website/src/routes/dataexport.js b/website/src/routes/dataexport.js index 5bf02a228c..d7328434a0 100644 --- a/website/src/routes/dataexport.js +++ b/website/src/routes/dataexport.js @@ -3,7 +3,7 @@ var router = new express.Router(); var dataexport = require('../controllers/dataexport'); var auth = require('../controllers/api-v2/auth'); var nconf = require('nconf'); -var i18n = require('../libs/i18n'); +var i18n = require('../libs/api-v2/i18n'); var locals = require('../middlewares/locals'); /* Data export */ diff --git a/website/src/routes/pages.js b/website/src/routes/pages.js index 27bc9a3619..7c847722e7 100644 --- a/website/src/routes/pages.js +++ b/website/src/routes/pages.js @@ -3,7 +3,7 @@ var express = require('express'); var router = new express.Router(); var _ = require('lodash'); var locals = require('../middlewares/locals'); -var i18n = require('../libs/i18n'); +var i18n = require('../libs/api-v2/i18n'); // -------- App -------- router.get('/', i18n.getUserLanguage, locals, function(req, res) { diff --git a/website/src/routes/payments.js b/website/src/routes/payments.js index 41c03210be..4989b113a1 100644 --- a/website/src/routes/payments.js +++ b/website/src/routes/payments.js @@ -3,7 +3,7 @@ var express = require('express'); var router = new express.Router(); var auth = require('../controllers/api-v2/auth'); var payments = require('../controllers/payments'); -var i18n = require('../libs/i18n'); +var i18n = require('../libs/api-v2/i18n'); router.get('/paypal/checkout', auth.authWithUrl, i18n.getUserLanguage, payments.paypalCheckout); router.get('/paypal/checkout/success', i18n.getUserLanguage, payments.paypalCheckoutSuccess); diff --git a/website/src/server.js b/website/src/server.js index 224022b192..5dd297638d 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -17,7 +17,7 @@ import attachMiddlewares from './middlewares/api-v3/index'; utils.setupConfig(); // Setup translations -// let i18n = require('./libs/i18n'); +// let i18n = require('./libs/api-v2/i18n'); const IS_PROD = nconf.get('IS_PROD'); // const IS_DEV = nconf.get('IS_DEV'); From eab9d7b3bab3740eb45c4fb2418381ac6ab3f0c7 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 09:15:11 -0600 Subject: [PATCH 04/35] Reorganize i18n tests. --- test/api/v3/unit/libs/i18n.test.js | 48 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/test/api/v3/unit/libs/i18n.test.js b/test/api/v3/unit/libs/i18n.test.js index 8acef06486..5bafc201dc 100644 --- a/test/api/v3/unit/libs/i18n.test.js +++ b/test/api/v3/unit/libs/i18n.test.js @@ -7,32 +7,40 @@ import fs from 'fs'; import path from 'path'; describe('i18n', () => { + let listOfLocales = []; + + before((done) => { + fs.readdir(localePath, (err, files) => { + if (err) return done(err); + + files.forEach((file) => { + if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return; + listOfLocales.push(file); + }); + + listOfLocales = listOfLocales.sort(); + done(); + }); + }); + describe('translations', () => { - it('loads all locales', (done) => { - fs.readdir(localePath, (err, files) => { - if (err) return done(err); - let locales = []; - - files.forEach((file) => { - if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return; - locales.push(file); - }); - - locales = locales.sort(); - let loaded = Object.keys(translations).sort(); - - expect(locales).to.eql(loaded); - done(); + it('includes a translation object for each locale', () => { + listOfLocales.forEach((locale) => { + expect(translations[locale]).to.be.an('object'); }); }); + }); - it('keeps a list all locales', () => { - expect(Object.keys(translations).sort()).to.eql(langCodes.sort()); + describe('localePath', () => { + it('is an absolute path to common/locales/', () => { + expect(localePath).to.match(/.*\/common\/locales\//); + expect(localePath) }); + }); - it('has an english translations', () => { - expect(langCodes).to.contain('en'); - expect(translations.en).to.be.an('object'); + describe('langCodes', () => { + it('is a list of all the language codes', () => { + expect(langCodes.sort()).to.eql(listOfLocales); }); }); }); From 939bc893c66a025ebeaf00657f8fa5444d35751b Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 09:21:22 -0600 Subject: [PATCH 05/35] Correct test to check req status after function is finished. --- test/api/v3/unit/middlewares/getUserLanguage.test.js | 5 +++-- website/src/middlewares/api-v3/getUserLanguage.js | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index 4a9bb0ec04..3346ef40d6 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -82,8 +82,9 @@ describe('getUserLanguage', () => { userId: 123 }; - getUserLanguage(req, res, next); - expect(req.language).to.equal('it'); + getUserLanguage(req, res, () => { + expect(req.language).to.equal('it'); + }); }); }); }); diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 813fe848fc..fd959cab94 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -75,7 +75,6 @@ export default function getUserLanguage (req, res, next) { .exec() .then((user) => { req.language = _getFromUser(user, req); - console.log(req.language); return next(); }) .catch(next); @@ -83,4 +82,4 @@ export default function getUserLanguage (req, res, next) { req.language = _getFromUser(null, req); return next(); } -} \ No newline at end of file +} From db67451a38401fd2de2c2fa5c3058a1537325d68 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 09:24:03 -0600 Subject: [PATCH 06/35] Add done callback to test. --- test/api/v3/unit/middlewares/getUserLanguage.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index 3346ef40d6..232a036cd3 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -77,13 +77,14 @@ describe('getUserLanguage', () => { }); describe('request with session', () => { - it('uses the user preferred language if avalaible', () => { + it('uses the user preferred language if avalaible', (done) => { req.session = { userId: 123 }; getUserLanguage(req, res, () => { expect(req.language).to.equal('it'); + done(); }); }); }); From a6a9c3c74f87d95569fa0c650272f7e362f9125e Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 09:27:17 -0600 Subject: [PATCH 07/35] Use context blocks instead of describe blocks. --- test/api/v3/unit/middlewares/getUserLanguage.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index 232a036cd3..7f5f9da7b3 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -28,7 +28,7 @@ describe('getUserLanguage', () => { }); }); - describe('query parameter', () => { + context('query parameter', () => { it('uses the language in the query parameter if avalaible', () => { req.query = { lang: 'es', @@ -48,7 +48,7 @@ describe('getUserLanguage', () => { }); }); - describe('authorized request', () => { + context('authorized request', () => { it('uses the user preferred language if avalaible', () => { req.locals = { user: { From 810a818334c42e3e5f96e05a7b268a19eef2d9e6 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 09:36:18 -0600 Subject: [PATCH 08/35] Add headers to req generator and unpend failing test. --- test/api/v3/unit/middlewares/getUserLanguage.test.js | 8 +++++--- test/helpers/api-unit.helper.js | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index 7f5f9da7b3..a8fbb88de7 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -62,7 +62,7 @@ describe('getUserLanguage', () => { expect(req.language).to.equal('it'); }); - xit('falls back to english if the user preferred language is not avalaible', () => { + it('falls back to english if the user preferred language is not avalaible', (done) => { req.locals = { user: { preferences: { @@ -71,8 +71,10 @@ describe('getUserLanguage', () => { }, }; - getUserLanguage(req, res, next); - expect(req.language).to.equal('en'); + getUserLanguage(req, res, () => { + expect(req.language).to.equal('en'); + done(); + }); }); }); diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index 2e789d53a9..62516f2751 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -35,6 +35,7 @@ export function generateReq(options={}) { let defaultReq = { body: {}, query: {}, + headers: {}, }; return defaults(options, defaultReq); From 8fd82c6808d3afad9b0e62eb589ff330d851a9ba Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 17:50:34 +0100 Subject: [PATCH 09/35] fix some requires --- website/src/libs/api-v2/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/libs/api-v2/analytics.js b/website/src/libs/api-v2/analytics.js index dff60a358a..3ea45256d9 100644 --- a/website/src/libs/api-v2/analytics.js +++ b/website/src/libs/api-v2/analytics.js @@ -1,7 +1,7 @@ require('../i18n'); var _ = require('lodash'); -var Content = require('../../../common').content; +var Content = require('../../../../common').content; var Amplitude = require('amplitude'); var googleAnalytics = require('universal-analytics'); From e84e3e135202ee6edda5413943933533bc2f5da0 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 18:06:50 +0100 Subject: [PATCH 10/35] fix another require path --- test/server_side/webhooks.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server_side/webhooks.test.js b/test/server_side/webhooks.test.js index ef6636bf93..45e042db8a 100644 --- a/test/server_side/webhooks.test.js +++ b/test/server_side/webhooks.test.js @@ -4,7 +4,7 @@ chai.use(require("sinon-chai")) var expect = chai.expect var rewire = require('rewire'); -var webhook = rewire('../../website/src/libs/webhook'); +var webhook = rewire('../../website/src/libs/api-v2/webhook'); describe('webhooks', function() { var postSpy; From 500c520c5983e05aa7bbdac88517dd2810f04a36 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 11:08:28 -0600 Subject: [PATCH 11/35] Move sandbox stub to test that uses it --- .../unit/middlewares/getUserLanguage.test.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index a8fbb88de7..a222e2e9c3 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -16,16 +16,6 @@ describe('getUserLanguage', () => { res = generateRes(); req = generateReq(); next = generateNext(); - - sandbox.stub(User, 'findOne').returns({ - exec() { - return Q.resolve({ - preferences: { - language: 'it', - } - }); - } - }); }); context('query parameter', () => { @@ -80,6 +70,16 @@ describe('getUserLanguage', () => { describe('request with session', () => { it('uses the user preferred language if avalaible', (done) => { + sandbox.stub(User, 'findOne').returns({ + exec() { + return Q.resolve({ + preferences: { + language: 'it', + } + }); + } + }); + req.session = { userId: 123 }; From 43058f1642838451654e67b82c841b984408ab8b Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 11:10:22 -0600 Subject: [PATCH 12/35] Add queries for which req pieces take precedence. --- .../unit/middlewares/getUserLanguage.test.js | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index a222e2e9c3..f587dafe04 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -36,6 +36,27 @@ describe('getUserLanguage', () => { getUserLanguage(req, res, next); expect(req.language).to.equal('en'); }); + + it('uses query even if the request includes a user and session', () => { + req.query = { + lang: 'es', + }; + + req.locals = { + user: { + preferences: { + language: 'it', + }, + }, + }; + + req.session = { + userId: 123 + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('es'); + }); }); context('authorized request', () => { @@ -66,9 +87,26 @@ describe('getUserLanguage', () => { done(); }); }); + + it('uses the user preferred language even if a session is included in request', () => { + req.locals = { + user: { + preferences: { + language: 'it', + }, + }, + }; + + req.session = { + userId: 123 + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('it'); + }); }); - describe('request with session', () => { + context('request with session', () => { it('uses the user preferred language if avalaible', (done) => { sandbox.stub(User, 'findOne').returns({ exec() { From 8372a56d888df56867af5126fbddd6d7d08e68ea Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 11:10:56 -0600 Subject: [PATCH 13/35] Adjust style of User.findOne call. --- website/src/middlewares/api-v3/getUserLanguage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index fd959cab94..475ff98742 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -68,8 +68,7 @@ export default function getUserLanguage (req, res, next) { req.language = _getFromUser(req.locals.user, req); return next(); } else if (req.session && req.session.userId) { // Same thing if the user has a valid session - User - .findOne({ + User.findOne({ _id: req.session.userId, }, 'preferences.language') .exec() From 06dd343d473bbe4fe4f338cc762cb35c9ebc1575 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 15:46:22 -0600 Subject: [PATCH 14/35] Adjust i18n script to actually display zh_TW language. --- website/src/libs/api-v2/i18n.js | 37 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/website/src/libs/api-v2/i18n.js b/website/src/libs/api-v2/i18n.js index b769afe697..335b4d5789 100644 --- a/website/src/libs/api-v2/i18n.js +++ b/website/src/libs/api-v2/i18n.js @@ -60,18 +60,38 @@ _.each(langCodes, function(code){ }catch (e){} }); -// Remove en_GB from langCodes checked by browser to avaoi it being +// Remove en_GB from langCodes checked by browser to avaoi it being // used in place of plain original 'en' var defaultLangCodes = _.without(langCodes, 'en_GB'); // A list of languages that have different versions var multipleVersionsLanguages = ['es', 'zh']; -var latinAmericanSpanishes = ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe', - 'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn', - 'es-ni', 'es-pr']; +var latinAmericanSpanishes = { + 'es-419': 'es_419', + 'es-mx': 'es_419', + 'es-gt': 'es_419', + 'es-cr': 'es_419', + 'es-pa': 'es_419', + 'es-do': 'es_419', + 'es-ve': 'es_419', + 'es-co': 'es_419', + 'es-pe': 'es_419', + 'es-ar': 'es_419', + 'es-ec': 'es_419', + 'es-cl': 'es_419', + 'es-uy': 'es_419', + 'es-py': 'es_419', + 'es-bo': 'es_419', + 'es-sv': 'es_419', + 'es-hn': 'es_419', + 'es-ni': 'es_419', + 'es-pr': 'es_419', +}; -var chineseVersions = ['zh-tw']; +var chineseVersions = { + 'zh-tw': 'zh_TW', +}; var getUserLanguage = function(req, res, next){ var getFromBrowser = function(){ @@ -97,10 +117,9 @@ var getUserLanguage = function(req, res, next){ } if(matches[0] === 'es'){ - return (latinAmericanSpanishes.indexOf(acceptedCompleteLang) !== -1) ? 'es_419' : 'es'; + return latinAmericanSpanishes[acceptedCompleteLang] || 'es'; }else if(matches[0] === 'zh'){ - var iChinese = chineseVersions.indexOf(acceptedCompleteLang.toLowerCase()); - return (iChinese !== -1) ? chineseVersions[iChinese] : 'zh'; + return chineseVersions[acceptedCompleteLang] || 'zh'; }else{ return en; } @@ -158,4 +177,4 @@ module.exports.enTranslations = function(){ // stringName and vars are the allow var args = Array.prototype.slice.call(arguments, 0); args.push(language.code); return shared.i18n.t.apply(null, args); -}; \ No newline at end of file +}; From dc8d52e00a7aca9e419f5a100de410dbdf19fe32 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 16:58:48 -0600 Subject: [PATCH 15/35] Correct paths in v2 lib --- website/src/libs/api-v2/i18n.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/libs/api-v2/i18n.js b/website/src/libs/api-v2/i18n.js index 335b4d5789..e8295deb03 100644 --- a/website/src/libs/api-v2/i18n.js +++ b/website/src/libs/api-v2/i18n.js @@ -1,12 +1,12 @@ var fs = require('fs'), path = require('path'), _ = require('lodash'), - User = require('../models/user').model, + User = require('../../models/user').model, accepts = require('accepts'), - shared = require('../../../common'), + shared = require('../../../../common'), translations = {}; -var localePath = path.join(__dirname, "/../../../common/locales/") +var localePath = path.join(__dirname, "/../../../../common/locales/") var loadTranslations = function(locale){ var files = fs.readdirSync(path.join(localePath, locale)); From f672ac8c59ff2d037a46a68d0c4f21265493cfa1 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 18:05:28 -0600 Subject: [PATCH 16/35] Port over change to v2 lib --- website/src/libs/api-v3/i18n.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js index e8a1b630cc..8b8c30e498 100644 --- a/website/src/libs/api-v3/i18n.js +++ b/website/src/libs/api-v3/i18n.js @@ -79,10 +79,30 @@ export let defaultLangCodes = _.without(langCodes, 'en_GB'); // A map of languages that have different versions and the relative versions export let multipleVersionsLanguages = { - es: ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe', - 'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn', - 'es-ni', 'es-pr'], - zh: ['zh-tw'], + es: { + 'es-419': 'es_419', + 'es-mx': 'es_419', + 'es-gt': 'es_419', + 'es-cr': 'es_419', + 'es-pa': 'es_419', + 'es-do': 'es_419', + 'es-ve': 'es_419', + 'es-co': 'es_419', + 'es-pe': 'es_419', + 'es-ar': 'es_419', + 'es-ec': 'es_419', + 'es-cl': 'es_419', + 'es-uy': 'es_419', + 'es-py': 'es_419', + 'es-bo': 'es_419', + 'es-sv': 'es_419', + 'es-hn': 'es_419', + 'es-ni': 'es_419', + 'es-pr': 'es_419', + }, + zh: { + 'zh-tw': 'zh_TW', + } }; // Export en strings only, temporary solution for mobile @@ -95,4 +115,4 @@ export function enTranslations (...args) { // language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined); args.push(language.code); return shared.i18n.t(...args); -} \ No newline at end of file +} From 4cd4c588a8db01ae12597d04648bf16c8b6339ec Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 18:05:45 -0600 Subject: [PATCH 17/35] Add tests for browser selection of language and refactor --- .../unit/middlewares/getUserLanguage.test.js | 110 ++++++++++++++++++ .../src/middlewares/api-v3/getUserLanguage.js | 51 ++++---- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index f587dafe04..2cbf1d2c87 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -128,4 +128,114 @@ describe('getUserLanguage', () => { }); }); }); + + context('browser fallback', () => { + it('uses browser specificed language', (done) => { + req.headers['accept-language'] = 'pt'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('pt'); + done(); + }); + }); + + it('uses first language in series if browser specifies multiple', (done) => { + req.headers['accept-language'] = 'he, pt, it'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('he'); + done(); + }); + }); + + it('skips invalid lanaguages and uses first language in series if browser specifies multiple', (done) => { + req.headers['accept-language'] = 'blah, he, pt, it'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('he'); + done(); + }); + }); + + it('uses normal version of language if specialized locale is passed in', (done) => { + req.headers['accept-language'] = 'fr-CA'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('fr'); + done(); + }); + }); + + it('uses normal version of language if specialized locale is passed in', (done) => { + req.headers['accept-language'] = 'fr-CA'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('fr'); + done(); + }); + }); + + it('uses es if es is passed in', (done) => { + req.headers['accept-language'] = 'es'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('es'); + done(); + }); + }); + + it('uses es_419 if applicable es-languages are passed in', (done) => { + req.headers['accept-language'] = 'es-mx'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('es_419'); + done(); + }); + }); + + it('uses es_419 if multiple es languages are passed in', (done) => { + req.headers['accept-language'] = 'es-GT, es-MX, es-CR'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('es_419'); + done(); + }); + }); + + it('zh', (done) => { + req.headers['accept-language'] = 'zh-TW'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('zh_TW'); + done(); + }); + }); + + it('uses english if browser specified language is not compatible', (done) => { + req.headers['accept-language'] = 'blah'; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('en'); + done(); + }); + }); + + it('uses english if browser does not specify', (done) => { + req.headers['accept-language'] = ''; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('en'); + done(); + }); + }); + + it('uses english if browser does not supply an accept-language header', (done) => { + delete req.headers['accept-language']; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('en'); + done(); + }); + }); + }); }); diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 475ff98742..4174d833ea 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -7,42 +7,43 @@ import { multipleVersionsLanguages, } from '../../libs/api-v3/i18n'; -function _getFromBrowser (req) { - let acceptedLanguages = accepts(req).languages(); - - let acceptable = _(acceptedLanguages).map((lang) => { +function _getUniqueListOfLanguages (languages) { + let acceptableLanguages = _(languages).map((lang) => { return lang.slice(0, 2); }).uniq().value(); - let matches = _.intersection(acceptable, defaultLangCodes); + let uniqueListOfLanguages = _.intersection(acceptableLanguages, defaultLangCodes); - let iAcceptedCompleteLang = matches.length > 0 ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1; + return uniqueListOfLanguages; +} - if (iAcceptedCompleteLang !== -1) { - let acceptedCompleteLang = _.find(acceptedLanguages, (accepted) => { - return accepted.slice(0, 2) === multipleVersionsLanguages[iAcceptedCompleteLang]; - }); +function _checkForApplicableLanguageVariant (originalLanguageOptions) { + let languageVariant = _.find(originalLanguageOptions, (accepted) => { + let trimmedAccepted = accepted.slice(0, 2); + return multipleVersionsLanguages[trimmedAccepted]; + }); - if (acceptedCompleteLang) { - acceptedCompleteLang = acceptedCompleteLang.toLowerCase(); + return languageVariant; +} + +function _getFromBrowser (req) { + let originalLanguageOptions = accepts(req).languages(); + let uniqueListOfLanguages = _getUniqueListOfLanguages(originalLanguageOptions); + let baseLanguage = (uniqueListOfLanguages[0] || '').toLowerCase(); + let languageMapping = multipleVersionsLanguages[baseLanguage]; + + if (languageMapping) { + let languageVariant = _checkForApplicableLanguageVariant(originalLanguageOptions); + + if (languageVariant) { + languageVariant = languageVariant.toLowerCase(); } else { return 'en'; } - if (matches[0] === 'es') { - // In case of a Latin American version of Spanish use 'es_419' - return multipleVersionsLanguages.es.indexOf(acceptedCompleteLang !== -1) ? 'es_419' : 'es'; - } else if (matches[0] === 'zh') { - let iChinese = multipleVersionsLanguages.zh.indexOf(acceptedCompleteLang.toLowerCase()); - - return iChinese !== -1 ? multipleVersionsLanguages.zh[iChinese] : 'zh'; - } else { - return 'en'; - } - } else if (matches.length > 0) { - return matches[0].toLowerCase(); + return languageMapping[languageVariant] || baseLanguage; } else { - return 'en'; + return baseLanguage || 'en'; } } From 805d4bba241dcf71ca1817f8de62d59204a86e90 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 18:29:19 -0600 Subject: [PATCH 18/35] Adjust for linter. --- website/src/libs/api-v3/i18n.js | 2 +- website/src/middlewares/api-v3/getUserLanguage.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js index 8b8c30e498..ed2134f7bf 100644 --- a/website/src/libs/api-v3/i18n.js +++ b/website/src/libs/api-v3/i18n.js @@ -102,7 +102,7 @@ export let multipleVersionsLanguages = { }, zh: { 'zh-tw': 'zh_TW', - } + }, }; // Export en strings only, temporary solution for mobile diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 4174d833ea..64d98475d5 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -20,6 +20,7 @@ function _getUniqueListOfLanguages (languages) { function _checkForApplicableLanguageVariant (originalLanguageOptions) { let languageVariant = _.find(originalLanguageOptions, (accepted) => { let trimmedAccepted = accepted.slice(0, 2); + return multipleVersionsLanguages[trimmedAccepted]; }); From 1ae9b7aff03ed0ce340a5d027b5598fdb03f8abb Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 18:40:46 -0600 Subject: [PATCH 19/35] Correct path to i18n --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index e2fc2b1083..18ce157de2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -129,7 +129,7 @@ module.exports = function(grunt) { grunt.registerTask('test:prepare:translations', function() { require('babel/register'); - var i18n = require('./website/src/libs/i18n'), + var i18n = require('./website/src/libs/api-v3/i18n'), fs = require('fs'); fs.writeFileSync('test/spec/mocks/translations.js', "if(!window.env) window.env = {};\n" + From 4a52f227741d5bcdb3a5dd4c55e1eff638581548 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 19:30:36 -0600 Subject: [PATCH 20/35] Correct path to old i18n file --- website/src/libs/api-v2/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/libs/api-v2/analytics.js b/website/src/libs/api-v2/analytics.js index 6f585eb2fe..6c1ccd3020 100644 --- a/website/src/libs/api-v2/analytics.js +++ b/website/src/libs/api-v2/analytics.js @@ -1,4 +1,4 @@ -require('./api-v2/i18n'); +require('./i18n'); var _ = require('lodash'); var Content = require('../../../../common').content; From 2e21d227e0906937beea93666c6bbdb5a2420631 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 13 Nov 2015 19:36:49 -0600 Subject: [PATCH 21/35] Simplify get language from user function. --- website/src/middlewares/api-v3/getUserLanguage.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 64d98475d5..234a78a29c 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -49,15 +49,8 @@ function _getFromBrowser (req) { } function _getFromUser (user, req) { - let lang; - - if (user && user.preferences.language && translations[user.preferences.language]) { - lang = user.preferences.language; - } else { - let preferred = _getFromBrowser(req); - - lang = translations[preferred] ? preferred : 'en'; - } + let preferredLang = user && user.preferences && user.preferences.language; + let lang = translations[preferredLang] ? preferredLang : _getFromBrowser(req); return lang; } From d89b1cb60dbf96a4a72c86328a8fffc6f71a65c0 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 02:50:58 +0100 Subject: [PATCH 22/35] port firebase lib to api v3 --- website/src/controllers/api-v2/groups.js | 2 +- website/src/controllers/api-v2/user.js | 2 +- website/src/libs/{ => api-v2}/firebase.js | 0 website/src/libs/api-v3/firebase.js | 68 +++++++++++++++++++++++ website/src/models/group.js | 2 +- website/src/server.js | 2 +- 6 files changed, 72 insertions(+), 4 deletions(-) rename website/src/libs/{ => api-v2}/firebase.js (100%) create mode 100644 website/src/libs/api-v3/firebase.js diff --git a/website/src/controllers/api-v2/groups.js b/website/src/controllers/api-v2/groups.js index 0fc969c3af..62ac3804b5 100644 --- a/website/src/controllers/api-v2/groups.js +++ b/website/src/controllers/api-v2/groups.js @@ -19,7 +19,7 @@ var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; var pushNotify = require('./../pushNotifications'); var analytics = utils.analytics; -var firebase = require('../../libs/firebase'); +var firebase = require('../../libs/api-v2/firebase'); /* ------------------------------------------------------------------------ diff --git a/website/src/controllers/api-v2/user.js b/website/src/controllers/api-v2/user.js index cd747e7d5c..5c6d256044 100644 --- a/website/src/controllers/api-v2/user.js +++ b/website/src/controllers/api-v2/user.js @@ -13,7 +13,7 @@ var moment = require('moment'); var logging = require('./../../libs/api-v2/logging'); var acceptablePUTPaths; var api = module.exports; -var firebase = require('../../libs/firebase'); +var firebase = require('../../libs/api-v2/firebase'); var webhook = require('../../libs/api-v2/webhook'); // api.purchase // Shared.ops diff --git a/website/src/libs/firebase.js b/website/src/libs/api-v2/firebase.js similarity index 100% rename from website/src/libs/firebase.js rename to website/src/libs/api-v2/firebase.js diff --git a/website/src/libs/api-v3/firebase.js b/website/src/libs/api-v3/firebase.js new file mode 100644 index 0000000000..ce20f3c7fc --- /dev/null +++ b/website/src/libs/api-v3/firebase.js @@ -0,0 +1,68 @@ +import Firebase from 'firebase'; +import nconf from 'nconf'; +const IS_PROD = nconf.get('IS_PROD'); +const FIREBASE_CONFIG = nconf.get('FIREBASE'); +const FIREBASE_ENABLED = IS_PROD && FIREBASE_CONFIG.ENABLED === 'true'; + +let firebaseRef; + +if (FIREBASE_ENABLED) { + firebaseRef = new Firebase(`https://${FIREBASE_CONFIG.APP}.firebaseio.com`); + + // TODO what happens if an op is sent before client is authenticated? + firebaseRef.authWithCustomToken(FIREBASE_CONFIG.SECRET, (err) => { + // TODO it's ok to kill the server here? what if FB is offline? + if (err) throw new Error('Impossible to authenticate Firebase'); + }); +} + +export function updateGroupData (group) { + if (!FIREBASE_ENABLED) return; + // TODO is throw ok? we don't have callbacks + if (!group) throw new Error('group obj is required.'); + // Return in case of tavern (comparison working because we use string for _id) + if (group._id === 'habitrpg') return; + + firebaseRef.child(`rooms/${group._id}`) + .set({ + name: group.name, + }); +} + +export function addUserToGroup (groupId, userId) { + if (!FIREBASE_ENABLED) return; + if (!userId || !groupId) throw new Error('groupId, userId are required.'); + if (groupId === 'habitrpg') return; + + firebaseRef.child(`members/${groupId}/${userId}`).set(true); + firebaseRef.child(`users/${userId}/rooms/${groupId}`).set(true); +} + +export function removeUserFromGroup (groupId, userId) { + if (!FIREBASE_ENABLED) return; + if (!userId || !groupId) throw new Error('groupId, userId are required.'); + if (groupId === 'habitrpg') return; + + firebaseRef.child(`members/${groupId}/${userId}`).remove(); + firebaseRef.child(`users/${userId}/rooms/${groupId}`).remove(); +} + +export function deleteGroup (groupId) { + if (!FIREBASE_ENABLED) return; + if (!groupId) throw new Error('groupId is required.'); + if (groupId === 'habitrpg') return; + + firebaseRef.child(`members/${groupId}`).remove(); + // FIXME not really necessary as long as we only store room data, + // as empty objects are automatically deleted (/members/... in future...) + firebaseRef.child(`rooms/${groupId}`).remove(); +} + +// FIXME not really necessary as long as we only store room data, +// as empty objects are automatically deleted +export function deleteUser (userId) { + if (!FIREBASE_ENABLED) return; + if (!userId) throw new Error('userId is required.'); + + firebaseRef.child(`users/${userId}`).remove(); +} \ No newline at end of file diff --git a/website/src/models/group.js b/website/src/models/group.js index b2da0011d7..0d3011cc38 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -6,7 +6,7 @@ var _ = require('lodash'); var async = require('async'); var logging = require('../libs/api-v2/logging'); var Challenge = require('./../models/challenge').model; -var firebase = require('../libs/firebase'); +var firebase = require('../libs/api-v2/firebase'); // NOTE any change to groups' members in MongoDB will have to be run through the API // changes made directly to the db will cause Firebase to get out of sync diff --git a/website/src/server.js b/website/src/server.js index 2562c9bb87..d8b98b10db 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -42,7 +42,7 @@ let db = mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, (err) => { autoinc.init(db); -import './libs/firebase'; +import './libs/api-v3/firebase'; // load schemas & models import './models/challenge'; From ae656b044c58d816367d4e1729dc8cef26031936 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 14:52:10 +0100 Subject: [PATCH 23/35] firebase can be enabled when not in production --- website/src/libs/api-v3/firebase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/libs/api-v3/firebase.js b/website/src/libs/api-v3/firebase.js index ce20f3c7fc..44b431ed46 100644 --- a/website/src/libs/api-v3/firebase.js +++ b/website/src/libs/api-v3/firebase.js @@ -2,7 +2,7 @@ import Firebase from 'firebase'; import nconf from 'nconf'; const IS_PROD = nconf.get('IS_PROD'); const FIREBASE_CONFIG = nconf.get('FIREBASE'); -const FIREBASE_ENABLED = IS_PROD && FIREBASE_CONFIG.ENABLED === 'true'; +const FIREBASE_ENABLED = FIREBASE_CONFIG.ENABLED === 'true'; let firebaseRef; From 9c8d4c383f391621308e0890293b03a3ca77f0b4 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 15:37:02 +0100 Subject: [PATCH 24/35] fix eslint, remove unused constant --- website/src/libs/api-v3/firebase.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/libs/api-v3/firebase.js b/website/src/libs/api-v3/firebase.js index 44b431ed46..92d6c28fc1 100644 --- a/website/src/libs/api-v3/firebase.js +++ b/website/src/libs/api-v3/firebase.js @@ -1,6 +1,5 @@ import Firebase from 'firebase'; import nconf from 'nconf'; -const IS_PROD = nconf.get('IS_PROD'); const FIREBASE_CONFIG = nconf.get('FIREBASE'); const FIREBASE_ENABLED = FIREBASE_CONFIG.ENABLED === 'true'; From 6343bc9f590617a31a2ef59d739e725c9fa311f9 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 15:36:08 +0100 Subject: [PATCH 25/35] port buildManifest lib --- .eslintrc | 1 - .../src/libs/{ => api-v2}/buildManifest.js | 6 +- website/src/libs/api-v3/buildManifest.js | 62 +++++++++++++++++++ website/src/middlewares/locals.js | 2 +- 4 files changed, 66 insertions(+), 5 deletions(-) rename website/src/libs/{ => api-v2}/buildManifest.js (91%) create mode 100644 website/src/libs/api-v3/buildManifest.js diff --git a/.eslintrc b/.eslintrc index 69c945431e..a3bc6debda 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,7 +28,6 @@ "no-new": 2, "no-octal-escape": 2, "no-octal": 2, - "no-param-reassign": 2, "no-process-env": 2, "no-proto": 2, "no-implied-eval": 2, diff --git a/website/src/libs/buildManifest.js b/website/src/libs/api-v2/buildManifest.js similarity index 91% rename from website/src/libs/buildManifest.js rename to website/src/libs/api-v2/buildManifest.js index e2b337860f..458bd8ac1e 100644 --- a/website/src/libs/buildManifest.js +++ b/website/src/libs/api-v2/buildManifest.js @@ -2,7 +2,7 @@ var fs = require('fs'); var path = require('path'); var nconf = require('nconf'); var _ = require('lodash'); -var manifestFiles = require("../../public/manifest.json"); +var manifestFiles = require("../../../public/manifest.json"); var IS_PROD = nconf.get('NODE_ENV') === 'production'; var buildFiles = []; @@ -15,7 +15,7 @@ var walk = function(folder){ if(fs.statSync(file).isDirectory()){ walk(file); }else{ - var relFolder = path.relative(path.join(__dirname, "/../../build"), folder); + var relFolder = path.relative(path.join(__dirname, "/../../../build"), folder); var old = fileName.replace(/-.{8}(\.[\d\w]+)$/, '$1'); if(relFolder){ @@ -28,7 +28,7 @@ var walk = function(folder){ }); }; -walk(path.join(__dirname, "/../../build")); +walk(path.join(__dirname, "/../../../build")); var getBuildUrl = module.exports.getBuildUrl = function(url){ if(buildFiles[url]) return '/' + buildFiles[url]; diff --git a/website/src/libs/api-v3/buildManifest.js b/website/src/libs/api-v3/buildManifest.js new file mode 100644 index 0000000000..94d6d49a2d --- /dev/null +++ b/website/src/libs/api-v3/buildManifest.js @@ -0,0 +1,62 @@ +import fs from 'fs'; +import path from 'path'; +import nconf from 'nconf'; + +const MANIFEST_FILE_PATH = path.join(__dirname, '/../../../public/manifest.json'); +const BUILD_FOLDER_PATH = path.join(__dirname, '/../../../build'); +let manifestFiles = require(MANIFEST_FILE_PATH); + +const IS_PROD = nconf.get('IS_PROD'); +let buildFiles = []; + +function _walk (folder) { + let files = fs.readdirSync(folder); + + files.forEach((fileName) => { + let file = `${folder}/${fileName}`; + + if (fs.statSync(file).isDirectory()) { + _walk(file); + } else { + let relFolder = path.relative(BUILD_FOLDER_PATH, folder); + let original = fileName.replace(/-.{8}(\.[\d\w]+)$/, '$1'); // Match the hash part of the filename + + if (relFolder) { + original = `${relFolder}/${original}`; + fileName = `${relFolder}/${fileName}`; + } + + buildFiles[original] = fileName; + } + }); +} + +// Walks through all the files in the build directory +// and creates a map of original files names and hashed files names +_walk(BUILD_FOLDER_PATH); + +export function getBuildUrl (url) { + return `/${buildFiles[url] || url}`; +} + +export function getManifestFiles (page) { + let files = manifestFiles[page]; + + if (!files) throw new Error(`Page "${page}" not found!`); + + let htmlCode = ''; + + if (IS_PROD) { + htmlCode += ``; // eslint-disable-line prefer-template + htmlCode += ``; // eslint-disable-line prefer-template + } else { + files.css.forEach((file) => { + htmlCode += ``; + }); + files.js.forEach((file) => { + htmlCode += ``; + }); + } + + return htmlCode; +} \ No newline at end of file diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index e6b2fa8818..7bec7cc399 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -3,7 +3,7 @@ var _ = require('lodash'); var utils = require('../libs/utils'); var shared = require('../../../common'); var i18n = require('../libs/api-v2/i18n'); -var buildManifest = require('../libs/buildManifest'); +var buildManifest = require('../libs/api-v2/buildManifest'); var shared = require('../../../common'); var forceRefresh = require('./forceRefresh'); var tavernQuest = require('../models/group').tavernQuest; From 466797cc6cf616849d582726f6dfdc917f4565fc Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 15:47:45 +0100 Subject: [PATCH 26/35] add some basic tests --- test/api/v3/unit/libs/buildManifest.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test/api/v3/unit/libs/buildManifest.test.js diff --git a/test/api/v3/unit/libs/buildManifest.test.js b/test/api/v3/unit/libs/buildManifest.test.js new file mode 100644 index 0000000000..5128427747 --- /dev/null +++ b/test/api/v3/unit/libs/buildManifest.test.js @@ -0,0 +1,18 @@ +import { + getManifestFiles, +} from '../../../../../website/src/libs/api-v3/buildManifest'; + +describe('Build Manifest', () => { + describe('getManifestFiles', () => { + it('returns an html string', () => { + let htmlCode = getManifestFiles('app'); + + expect(htmlCode).to.be.a.String; + }); + + it('throws an error in case the page does not exist', () => { + let getManifestFilesFn = () => { getManifestFiles('strange name here') }; + expect(getManifestFilesFn).to.throw(Error); + }); + }); +}); From b315d10c79d5389347210738dbf0d4da7835b84d Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 16:09:51 +0100 Subject: [PATCH 27/35] migrate utils to v3, uprade nodemailer --- .eslintrc | 1 - package.json | 4 +- tasks/gulp-console.js | 2 +- test/server_side/controllers/groups.test.js | 2 +- website/src/controllers/api-v2/auth.js | 2 +- website/src/controllers/api-v2/challenges.js | 2 +- website/src/controllers/api-v2/groups.js | 2 +- website/src/controllers/api-v2/members.js | 2 +- .../src/controllers/api-v2/unsubscription.js | 2 +- website/src/controllers/api-v2/user.js | 2 +- website/src/controllers/payments/index.js | 2 +- website/src/index.js | 4 +- website/src/libs/{ => api-v2}/utils.js | 0 website/src/libs/api-v3/email.js | 153 ++++++++++++++++++ website/src/libs/api-v3/encryption.js | 24 +++ website/src/middlewares/locals.js | 2 +- website/src/server.js | 2 +- 17 files changed, 192 insertions(+), 16 deletions(-) rename website/src/libs/{ => api-v2}/utils.js (100%) create mode 100644 website/src/libs/api-v3/email.js create mode 100644 website/src/libs/api-v3/encryption.js diff --git a/.eslintrc b/.eslintrc index 69c945431e..a3bc6debda 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,7 +28,6 @@ "no-new": 2, "no-octal-escape": 2, "no-octal": 2, - "no-param-reassign": 2, "no-process-env": 2, "no-proto": 2, "no-implied-eval": 2, diff --git a/package.json b/package.json index 40c04bdfbb..8e0603e0db 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "babel": "^5.5.4", "babelify": "^7.2.0", "body-parser": "^1.14.1", - "compression": "^1.6.0", "bower": "~1.3.12", "browserify": "~12.0.1", "coffee-script": "1.6.x", + "compression": "^1.6.0", "connect-ratelimit": "0.0.7", "cookie-parser": "^1.4.0", "cookie-session": "^1.2.0", @@ -64,7 +64,7 @@ "nconf": "~0.8.2", "newrelic": "~1.23.0", "nib": "~1.0.1", - "nodemailer": "~0.5.2", + "nodemailer": "^1.9.0", "pageres": "^1.0.1", "passport": "~0.2.1", "passport-facebook": "2.0.0", diff --git a/tasks/gulp-console.js b/tasks/gulp-console.js index 46f4ea44ea..d5d318c28e 100644 --- a/tasks/gulp-console.js +++ b/tasks/gulp-console.js @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import autoinc from 'mongoose-id-autoinc'; import logger from '../website/src/libs/api-v3/logger'; import nconf from 'nconf'; -import utils from '../website/src/libs/utils'; +import utils from '../website/src/libs/api-v2/utils'; import repl from 'repl'; import gulp from 'gulp'; diff --git a/test/server_side/controllers/groups.test.js b/test/server_side/controllers/groups.test.js index 9e970c4afc..9574ed7640 100644 --- a/test/server_side/controllers/groups.test.js +++ b/test/server_side/controllers/groups.test.js @@ -8,7 +8,7 @@ var Group = require('../../../website/src/models/group').model; var groupsController = require('../../../website/src/controllers/api-v2/groups'); describe('Groups Controller', function() { - var utils = require('../../../website/src/libs/utils'); + var utils = require('../../../website/src/libs/api-v2/utils'); describe('#invite', function() { var res, req, user, group; diff --git a/website/src/controllers/api-v2/auth.js b/website/src/controllers/api-v2/auth.js index 11cab90624..dd379cc095 100644 --- a/website/src/controllers/api-v2/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -3,7 +3,7 @@ var validator = require('validator'); var passport = require('passport'); var shared = require('../../../../common'); var async = require('async'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var nconf = require('nconf'); var request = require('request'); var FirebaseTokenGenerator = require('firebase-token-generator'); diff --git a/website/src/controllers/api-v2/challenges.js b/website/src/controllers/api-v2/challenges.js index 21104f2885..89d68495d6 100644 --- a/website/src/controllers/api-v2/challenges.js +++ b/website/src/controllers/api-v2/challenges.js @@ -9,7 +9,7 @@ var Group = require('./../../models/group').model; var Challenge = require('./../../models/challenge').model; var logging = require('./../../libs/api-v2/logging'); var csv = require('express-csv'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var api = module.exports; var pushNotify = require('./../pushNotifications'); diff --git a/website/src/controllers/api-v2/groups.js b/website/src/controllers/api-v2/groups.js index 62ac3804b5..0355148856 100644 --- a/website/src/controllers/api-v2/groups.js +++ b/website/src/controllers/api-v2/groups.js @@ -9,7 +9,7 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var Q = require('q'); -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var shared = require('../../../../common'); var User = require('./../../models/user').model; var Group = require('./../../models/group').model; diff --git a/website/src/controllers/api-v2/members.js b/website/src/controllers/api-v2/members.js index c528b92cd7..1c3bb448ac 100644 --- a/website/src/controllers/api-v2/members.js +++ b/website/src/controllers/api-v2/members.js @@ -5,7 +5,7 @@ var api = module.exports; var async = require('async'); var _ = require('lodash'); var shared = require('../../../../common'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var nconf = require('nconf'); var pushNotify = require('./../pushNotifications'); diff --git a/website/src/controllers/api-v2/unsubscription.js b/website/src/controllers/api-v2/unsubscription.js index 772d0580e9..65f37f6298 100644 --- a/website/src/controllers/api-v2/unsubscription.js +++ b/website/src/controllers/api-v2/unsubscription.js @@ -1,6 +1,6 @@ var User = require('../../models/user').model; var EmailUnsubscription = require('../../models/emailUnsubscription').model; -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var i18n = require('../../../../common').i18n; var api = module.exports = {}; diff --git a/website/src/controllers/api-v2/user.js b/website/src/controllers/api-v2/user.js index 5c6d256044..519ae0c557 100644 --- a/website/src/controllers/api-v2/user.js +++ b/website/src/controllers/api-v2/user.js @@ -5,7 +5,7 @@ var nconf = require('nconf'); var async = require('async'); var shared = require('../../../../common'); var User = require('./../../models/user').model; -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var analytics = utils.analytics; var Group = require('./../../models/group').model; var Challenge = require('./../../models/challenge').model; diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js index 866c33381b..074fab2369 100644 --- a/website/src/controllers/payments/index.js +++ b/website/src/controllers/payments/index.js @@ -1,7 +1,7 @@ var _ = require('lodash'); var shared = require('../../../../common'); var nconf = require('nconf'); -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var moment = require('moment'); var isProduction = nconf.get("NODE_ENV") === "production"; var stripe = require('./stripe'); diff --git a/website/src/index.js b/website/src/index.js index 134060bf45..1de0170cd1 100644 --- a/website/src/index.js +++ b/website/src/index.js @@ -9,13 +9,13 @@ var logger = require('./libs/api-v3/logger'); // Initialize configuration var setupNconf = require('./libs/api-v3/setupNconf'); setupNconf(); -var utils = require('./libs/utils'); -utils.setupConfig(); var IS_PROD = nconf.get('IS_PROD'); var IS_DEV = nconf.get('IS_DEV'); var cores = Number(nconf.get('WEB_CONCURRENCY')) || 0; +if (IS_DEV) Error.stackTraceLimit = Infinity; + // Setup the cluster module if (cores !== 0 && cluster.isMaster && (IS_DEV || IS_PROD)) { // Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production) diff --git a/website/src/libs/utils.js b/website/src/libs/api-v2/utils.js similarity index 100% rename from website/src/libs/utils.js rename to website/src/libs/api-v2/utils.js diff --git a/website/src/libs/api-v3/email.js b/website/src/libs/api-v3/email.js new file mode 100644 index 0000000000..8d0cff0a8c --- /dev/null +++ b/website/src/libs/api-v3/email.js @@ -0,0 +1,153 @@ +import { createTransport } from 'nodemailer'; +import nconf from 'nconf'; +import logger from './logger'; +import { encrypt } from './encryption'; +import request from 'request'; + +const IS_PROD = nconf.get('IS_PROD'); +const EMAIL_SERVER = { + url: nconf.get('EMAIL_SERVER:url'), + auth: { + user: nconf.get('EMAIL_SERVER:authUser'), + password: nconf.get('EMAIL_SERVER:authPassword'), + }, +}; +const BASE_URL = nconf.get('BASE_URL'); + +let smtpTransporter = createTransport({ + service: nconf.get('SMTP_SERVICE'), + auth: { + user: nconf.get('SMTP_USER'), + pass: nconf.get('SMTP_PASS'), + }, +}); + +// Send email directly from the server using the smtpTransporter, +// used only to send password reset emails because users unsubscribed on Mandrill wouldn't get them +export function send (mailData) { + return smtpTransporter + .sendMail(mailData) + .catch((error) => logger.error(error)); +} + +export function getUserInfo (user, fields) { + let info = {}; + + if (fields.indexOf('name') !== -1) { + if (user.auth.local) { + info.name = user.profile.name || user.auth.local.username; + } else if (user.auth.facebook) { + info.name = user.profile.name || user.auth.facebook.displayName || user.auth.facebook.username; + } + } + + if (fields.indexOf('email') !== -1) { + if (user.auth.local && user.auth.local.email) { + info.email = user.auth.local.email; + } else if (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value) { + info.email = user.auth.facebook.emails[0].value; + } + } + + if (fields.indexOf('_id') !== -1) { + info._id = user._id; + } + + if (fields.indexOf('canSend') !== -1) { + info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true; + } + + return info; +} + +// Send a transactional email using Mandrill through the external email server +export function txnEmail (mailingInfoArray, emailType, variables, personalVariables) { + mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; + + variables = [ + {name: 'BASE_URL', content: BASE_URL}, + ].concat(variables || []); + + // It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed + mailingInfoArray = mailingInfoArray.map((mailingInfo) => { + return mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo; + }).filter((mailingInfo) => { + // Always send reset-password emails + // Don't check canSend for non registered users as already checked before + return mailingInfo.email && (!mailingInfo._id || mailingInfo.canSend || emailType === 'reset-password'); + }); + + // Personal variables are personal to each email recipient, if they are missing + // we manually create a structure for them with RECIPIENT_NAME and RECIPIENT_UNSUB_URL + // otherwise we just add RECIPIENT_NAME and RECIPIENT_UNSUB_URL to the existing personal variables + if (!personalVariables || personalVariables.length === 0) { + personalVariables = mailingInfoArray.map((mailingInfo) => { + return { + rcpt: mailingInfo.email, + vars: [ + { + name: 'RECIPIENT_NAME', + content: mailingInfo.name, + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: `/unsubscribe?code=${encrypt(JSON.stringify({ + _id: mailingInfo._id, + email: mailingInfo.email, + }))}`, + }, + ], + }; + }); + } else { + let temporaryPersonalVariables = {}; + + mailingInfoArray.forEach((mailingInfo) => { + temporaryPersonalVariables[mailingInfo.email] = { + name: mailingInfo.name, + _id: mailingInfo._id, + }; + }); + + personalVariables.forEach((singlePersonalVariables) => { + singlePersonalVariables.vars.push( + { + name: 'RECIPIENT_NAME', + content: temporaryPersonalVariables[singlePersonalVariables.rcpt].name, + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: `/unsubscribe?code=${encrypt(JSON.stringify({ + _id: temporaryPersonalVariables[singlePersonalVariables.rcpt]._id, + email: singlePersonalVariables.rcpt, + }))}`, + } + ); + }); + } + + if (IS_PROD && mailingInfoArray.length > 0) { + request({ + url: `${EMAIL_SERVER.url}/job`, + method: 'POST', + auth: { + user: EMAIL_SERVER.auth.user, + pass: EMAIL_SERVER.auth.password, + }, + json: { + type: 'email', + data: { + emailType, + to: mailingInfoArray, + variables, + personalVariables, + }, + options: { + priority: 'high', + attempts: 5, + backoff: {delay: 10 * 60 * 1000, type: 'fixed'}, + }, + }, + }, (err) => logger.error(err)); + } +} diff --git a/website/src/libs/api-v3/encryption.js b/website/src/libs/api-v3/encryption.js new file mode 100644 index 0000000000..4e370bb9a8 --- /dev/null +++ b/website/src/libs/api-v3/encryption.js @@ -0,0 +1,24 @@ +import { + createCipher, + createDecipher, +} from 'crypto'; +import nconf from 'nconf'; + +const algorithm = 'aes-256-ctr'; +const SESSION_SECRET = nconf.get('SESSION_SECRET'); + +export function encrypt (text) { + let cipher = createCipher(algorithm, SESSION_SECRET); + let crypted = cipher.update(text, 'utf8', 'hex'); + + crypted += cipher.final('hex'); + return crypted; +} + +export function decrypt (text) { + let decipher = createDecipher(algorithm, SESSION_SECRET); + let dec = decipher.update(text, 'hex', 'utf8'); + + dec += decipher.final('utf8'); + return dec; +} \ No newline at end of file diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index e6b2fa8818..e5caea707b 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -1,6 +1,6 @@ var nconf = require('nconf'); var _ = require('lodash'); -var utils = require('../libs/utils'); +var utils = require('../libs/api-v2/utils'); var shared = require('../../../common'); var i18n = require('../libs/api-v2/i18n'); var buildManifest = require('../libs/buildManifest'); diff --git a/website/src/server.js b/website/src/server.js index e9f7ddbbaa..e6a45aa743 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -2,7 +2,7 @@ import nconf from 'nconf'; import logger from './libs/api-v3/logger'; -import utils from './libs/utils'; +import utils from './libs/api-v2/utils'; import express from 'express'; import http from 'http'; // import path from 'path'; From 170a25c7122168ee10ba757373c25cc396bf1c78 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 16:12:32 +0100 Subject: [PATCH 28/35] remove v2 utils where possible --- tasks/gulp-console.js | 3 --- website/src/server.js | 2 -- 2 files changed, 5 deletions(-) diff --git a/tasks/gulp-console.js b/tasks/gulp-console.js index d5d318c28e..96af26a27a 100644 --- a/tasks/gulp-console.js +++ b/tasks/gulp-console.js @@ -2,7 +2,6 @@ import mongoose from 'mongoose'; import autoinc from 'mongoose-id-autoinc'; import logger from '../website/src/libs/api-v3/logger'; import nconf from 'nconf'; -import utils from '../website/src/libs/api-v2/utils'; import repl from 'repl'; import gulp from 'gulp'; @@ -19,8 +18,6 @@ let improveRepl = (context) => { process.stdout.write('\u001B[2J\u001B[0;0f'); }}); - utils.setupConfig(); - context.Challenge = require('../website/src/models/challenge').model; context.Group = require('../website/src/models/group').model; context.User = require('../website/src/models/user').model; diff --git a/website/src/server.js b/website/src/server.js index e6a45aa743..657600b39c 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -2,7 +2,6 @@ import nconf from 'nconf'; import logger from './libs/api-v3/logger'; -import utils from './libs/api-v2/utils'; import express from 'express'; import http from 'http'; // import path from 'path'; @@ -14,7 +13,6 @@ import passportFacebook from 'passport-facebook'; import mongoose from 'mongoose'; import Q from 'q'; import attachMiddlewares from './middlewares/api-v3/index'; -utils.setupConfig(); // Setup translations // let i18n = require('./libs/api-v2/i18n'); From 19ce7c9b53b5cb813487a210c5b6a9021fb04ca3 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 17:04:47 +0100 Subject: [PATCH 29/35] add tests for emails (wip) and encryption, misc fixes --- website/src/libs/api-v3/email.js | 23 ++++++++++++++--------- website/src/libs/api-v3/encryption.js | 1 + website/src/libs/api-v3/logger.js | 4 +++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/website/src/libs/api-v3/email.js b/website/src/libs/api-v3/email.js index 8d0cff0a8c..65cbcce02a 100644 --- a/website/src/libs/api-v3/email.js +++ b/website/src/libs/api-v3/email.js @@ -30,14 +30,18 @@ export function send (mailData) { .catch((error) => logger.error(error)); } -export function getUserInfo (user, fields) { +export function getUserInfo (user, fields = []) { let info = {}; if (fields.indexOf('name') !== -1) { - if (user.auth.local) { - info.name = user.profile.name || user.auth.local.username; - } else if (user.auth.facebook) { - info.name = user.profile.name || user.auth.facebook.displayName || user.auth.facebook.username; + info.name = user.profile && user.profile.name; + + if (!info.name) { + if (user.auth.local && user.auth.local.username) { + info.name = user.auth.local.username; + } else if (user.auth.facebook) { + info.name = user.auth.facebook.displayName || user.auth.facebook.username; + } } } @@ -54,14 +58,16 @@ export function getUserInfo (user, fields) { } if (fields.indexOf('canSend') !== -1) { - info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true; + if (user.preferences && user.preferences.emailNotifications) { + info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true; + } } return info; } // Send a transactional email using Mandrill through the external email server -export function txnEmail (mailingInfoArray, emailType, variables, personalVariables) { +export function sendTxn (mailingInfoArray, emailType, variables, personalVariables) { mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; variables = [ @@ -127,9 +133,8 @@ export function txnEmail (mailingInfoArray, emailType, variables, personalVariab } if (IS_PROD && mailingInfoArray.length > 0) { - request({ + request.post({ url: `${EMAIL_SERVER.url}/job`, - method: 'POST', auth: { user: EMAIL_SERVER.auth.user, pass: EMAIL_SERVER.auth.password, diff --git a/website/src/libs/api-v3/encryption.js b/website/src/libs/api-v3/encryption.js index 4e370bb9a8..0f5f9d83dd 100644 --- a/website/src/libs/api-v3/encryption.js +++ b/website/src/libs/api-v3/encryption.js @@ -4,6 +4,7 @@ import { } from 'crypto'; import nconf from 'nconf'; +// TODO check this is secure const algorithm = 'aes-256-ctr'; const SESSION_SECRET = nconf.get('SESSION_SECRET'); diff --git a/website/src/libs/api-v3/logger.js b/website/src/libs/api-v3/logger.js index 4583610d25..3ab20ad685 100644 --- a/website/src/libs/api-v3/logger.js +++ b/website/src/libs/api-v3/logger.js @@ -12,7 +12,9 @@ if (IS_PROD) { // log errors to console too } else { logger - .add(winston.transports.Console); + .add(winston.transports.Console, { + colorize: true, + }); } export default logger; From 3b75fe6adeec45c5c34298e541aff4cfeb6d4f76 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 13 Nov 2015 17:27:13 +0100 Subject: [PATCH 30/35] add tests for emails (wip) and encryption, misc fixes --- test/api/v3/unit/libs/email.test.js | 94 ++++++++++++++++++++++++ test/api/v3/unit/libs/encryption.test.js | 15 ++++ 2 files changed, 109 insertions(+) create mode 100644 test/api/v3/unit/libs/email.test.js create mode 100644 test/api/v3/unit/libs/encryption.test.js diff --git a/test/api/v3/unit/libs/email.test.js b/test/api/v3/unit/libs/email.test.js new file mode 100644 index 0000000000..731f24e347 --- /dev/null +++ b/test/api/v3/unit/libs/email.test.js @@ -0,0 +1,94 @@ +import request from 'request'; +import { + send as sendEmail, + sendTxn as sendTxnEmail, + getUserInfo, +} from '../../../../../website/src/libs/api-v3/email'; + +function getUser () { + return { + _id: 'random _id', + auth: { + local: { + username: 'username', + email: 'email@email', + }, + facebook: { + emails: [{ + value: 'email@facebook' + }], + displayName: 'fb display name', + } + }, + profile: { + name: 'profile name', + }, + preferences: { + emailNotifications: { + unsubscribeFromAll: false + }, + }, + }; +}; + +describe('emails', () => { + + beforeEach(() => { + sandbox.stub(request, 'post'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('sendEmail', () => { + + }); + + describe('getUserInfo', () => { + it('returns an empty object if no field request', () => { + expect(getUserInfo({}, [])).to.be.empty; + }); + + it('returns correct user data', () => { + let user = getUser(); + let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); + + expect(data).to.have.property('name', user.profile.name); + expect(data).to.have.property('email', user.auth.local.email); + expect(data).to.have.property('_id', user._id); + expect(data).to.have.property('canSend', true); + }); + + it('returns correct user data [facebook users]', () => { + let user = getUser(); + delete user.profile['name']; + delete user.auth['local']; + + let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); + + expect(data).to.have.property('name', user.auth.facebook.displayName); + expect(data).to.have.property('email', user.auth.facebook.emails[0].value); + expect(data).to.have.property('_id', user._id); + expect(data).to.have.property('canSend', true); + }); + + it('has fallbacks for missing data', () => { + let user = getUser(); + delete user.profile['name']; + delete user.auth.local['email'] + delete user.auth['facebook']; + + let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); + + expect(data).to.have.property('name', user.auth.local.username); + expect(data).not.to.have.property('email'); + expect(data).to.have.property('_id', user._id); + expect(data).to.have.property('canSend', true); + }); + }); + + describe('sendTxnEmail', () => { + + }); +}); diff --git a/test/api/v3/unit/libs/encryption.test.js b/test/api/v3/unit/libs/encryption.test.js new file mode 100644 index 0000000000..dcab9bffd3 --- /dev/null +++ b/test/api/v3/unit/libs/encryption.test.js @@ -0,0 +1,15 @@ +import { + encrypt, + decrypt, +} from '../../../../../website/src/libs/api-v3/encryption'; + +describe('encryption', () => { + it('can encrypt and decrypt', () => { + let data = 'some secret text'; + let encrypted = encrypt(data); + let decrypted = decrypt(encrypted); + + expect(encrypted).not.to.equal(data); + expect(data).to.equal(decrypted); + }); +}); From a8b3780cc0bf768bd2f80a3b84e48dde4f761a34 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 18:13:17 +0100 Subject: [PATCH 31/35] add send email tests --- test/api/v3/unit/libs/email.test.js | 50 +++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/test/api/v3/unit/libs/email.test.js b/test/api/v3/unit/libs/email.test.js index 731f24e347..5760fd2a99 100644 --- a/test/api/v3/unit/libs/email.test.js +++ b/test/api/v3/unit/libs/email.test.js @@ -1,9 +1,8 @@ import request from 'request'; -import { - send as sendEmail, - sendTxn as sendTxnEmail, - getUserInfo, -} from '../../../../../website/src/libs/api-v3/email'; +import nconf from 'nconf'; +import nodemailer from 'nodemailer'; +import Q from 'q'; +import logger from '../../../../../website/src/libs/api-v3/logger'; function getUser () { return { @@ -32,8 +31,10 @@ function getUser () { }; describe('emails', () => { + let pathToEmailLib = '../../../../../website/src/libs/api-v3/email'; beforeEach(() => { + delete require.cache[require.resolve(pathToEmailLib)]; sandbox.stub(request, 'post'); }); @@ -42,15 +43,48 @@ describe('emails', () => { }); describe('sendEmail', () => { + it('can send an email using the default transport', () => { + let sendMailSpy = sandbox.stub().returns(Q.defer().promise); + sandbox.stub(nodemailer, 'createTransport').returns({ + sendMail: sendMailSpy, + }); + + let attachEmail = require(pathToEmailLib); + attachEmail.send(); + expect(sendMailSpy).to.be.calledOnce; + }); + + it('logs errors', (done) => { + let deferred = Q.defer(); + let sendMailSpy = sandbox.stub().returns(deferred.promise); + + sandbox.stub(nodemailer, 'createTransport').returns({ + sendMail: sendMailSpy, + }); + sandbox.stub(logger, 'error'); + + let attachEmail = require(pathToEmailLib); + attachEmail.send(); + expect(sendMailSpy).to.be.calledOnce; + deferred.reject(); + deferred.promise.catch((err) => { + expect(logger.error).to.be.calledOnce; + done(); + }); + }); }); describe('getUserInfo', () => { it('returns an empty object if no field request', () => { + let attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; expect(getUserInfo({}, [])).to.be.empty; }); it('returns correct user data', () => { + let attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; let user = getUser(); let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); @@ -61,6 +95,8 @@ describe('emails', () => { }); it('returns correct user data [facebook users]', () => { + let attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; let user = getUser(); delete user.profile['name']; delete user.auth['local']; @@ -74,6 +110,8 @@ describe('emails', () => { }); it('has fallbacks for missing data', () => { + let attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; let user = getUser(); delete user.profile['name']; delete user.auth.local['email'] @@ -89,6 +127,6 @@ describe('emails', () => { }); describe('sendTxnEmail', () => { - + it }); }); From a80be380f9018a06f690ee8a95aa5957898976ff Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sat, 14 Nov 2015 18:58:33 +0100 Subject: [PATCH 32/35] add txt emails tests --- test/api/v3/unit/libs/email.test.js | 97 +++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/test/api/v3/unit/libs/email.test.js b/test/api/v3/unit/libs/email.test.js index 5760fd2a99..353ac0ad09 100644 --- a/test/api/v3/unit/libs/email.test.js +++ b/test/api/v3/unit/libs/email.test.js @@ -35,11 +35,6 @@ describe('emails', () => { beforeEach(() => { delete require.cache[require.resolve(pathToEmailLib)]; - sandbox.stub(request, 'post'); - }); - - afterEach(() => { - sandbox.restore(); }); describe('sendEmail', () => { @@ -127,6 +122,96 @@ describe('emails', () => { }); describe('sendTxnEmail', () => { - it + beforeEach(() => { + sandbox.stub(request, 'post'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('can send a txn email to one recipient', () => { + sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); + let attachEmail = require(pathToEmailLib); + let sendTxnEmail = attachEmail.sendTxn; + let emailType = 'an email type'; + let mailingInfo = { + name: 'my name', + email: 'my@email', + }; + + sendTxnEmail(mailingInfo, emailType); + expect(request.post).to.be.calledWith(sinon.match({ + json: { + data: { + emailType: sinon.match.same(emailType), + to: sinon.match((value) => { + return Array.isArray(value) && value[0].name === mailingInfo.name; + }, 'matches mailing info array'), + } + } + })); + }); + + it('does not send email if address is missing', () => { + sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); + let attachEmail = require(pathToEmailLib); + let sendTxnEmail = attachEmail.sendTxn; + let emailType = 'an email type'; + let mailingInfo = { + name: 'my name', + //email: 'my@email', + }; + + sendTxnEmail(mailingInfo, emailType); + expect(request.post).not.to.be.called; + }); + + it('uses getUserInfo in case of user data', () => { + sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); + let attachEmail = require(pathToEmailLib); + let sendTxnEmail = attachEmail.sendTxn; + let emailType = 'an email type'; + let mailingInfo = getUser(); + + sendTxnEmail(mailingInfo, emailType); + expect(request.post).to.be.calledWith(sinon.match({ + json: { + data: { + emailType: sinon.match.same(emailType), + to: sinon.match(val => val[0]._id === mailingInfo._id), + } + } + })); + }); + + it('sends email with some default variables', () => { + sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); + let attachEmail = require(pathToEmailLib); + let sendTxnEmail = attachEmail.sendTxn; + let emailType = 'an email type'; + let mailingInfo = { + name: 'my name', + email: 'my@email', + }; + let variables = [1,2,3]; + + sendTxnEmail(mailingInfo, emailType, variables); + expect(request.post).to.be.calledWith(sinon.match({ + json: { + data: { + variables: sinon.match((value) => { + return value[0].name === 'BASE_URL'; + }, 'matches variables'), + personalVariables: sinon.match((value) => { + return (value[0].rcpt === mailingInfo.email + && value[0].vars[0].name === 'RECIPIENT_NAME' + && value[0].vars[1].name === 'RECIPIENT_UNSUB_URL' + ); + }, 'matches personal variables'), + } + } + })); + }); }); }); From 78e5d913f5063676e41eaea79f7f494bc4e6cfe6 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 15 Nov 2015 17:27:22 +0100 Subject: [PATCH 33/35] update test to match possible html string --- test/api/v3/unit/libs/buildManifest.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/v3/unit/libs/buildManifest.test.js b/test/api/v3/unit/libs/buildManifest.test.js index 5128427747..d03e19c9eb 100644 --- a/test/api/v3/unit/libs/buildManifest.test.js +++ b/test/api/v3/unit/libs/buildManifest.test.js @@ -7,7 +7,7 @@ describe('Build Manifest', () => { it('returns an html string', () => { let htmlCode = getManifestFiles('app'); - expect(htmlCode).to.be.a.String; + expect(htmlCode.startsWith(' { From 5dc2fb0b6f06b76a91ffd5519bda5dcdce9006fd Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 15 Nov 2015 18:12:55 +0100 Subject: [PATCH 34/35] port domainMiddleware --- website/src/middlewares/api-v3/domain.js | 16 ++++++++++++++++ website/src/server.js | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 website/src/middlewares/api-v3/domain.js diff --git a/website/src/middlewares/api-v3/domain.js b/website/src/middlewares/api-v3/domain.js new file mode 100644 index 0000000000..63272381da --- /dev/null +++ b/website/src/middlewares/api-v3/domain.js @@ -0,0 +1,16 @@ +// TODO in api-v2 this module also checked memory usage every x minutes and +// threw an error in case of low memory avalible (possible memory leak) +// it's yet to be decided whether to keep it or not +import domainMiddleware from 'domain-middleware'; + +export default function implementDomainMiddleware (server, mongoose) { + return domainMiddleware({ + server: { + close () { + server.close(); + mongoose.connection.close(); + }, + }, + killTimeout: 10000, + }); +} \ No newline at end of file diff --git a/website/src/server.js b/website/src/server.js index e9f7ddbbaa..2e39f50d05 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -13,6 +13,7 @@ import passport from 'passport'; import passportFacebook from 'passport-facebook'; import mongoose from 'mongoose'; import Q from 'q'; +import domainMiddleware from './middlewares/api-v3/domain'; import attachMiddlewares from './middlewares/api-v3/index'; utils.setupConfig(); @@ -84,6 +85,7 @@ app.set('port', nconf.get('PORT')); let oldApp = express(); // api v1 and v2, and not scoped routes let newApp = express(); // api v3 +app.use(domainMiddleware(server, mongoose)); // Route requests to the right app // Matches all request except the ones going to /api/v3/** app.all(/^(?!\/api\/v3).+/i, oldApp); From 1ccb9a5aaa03a173e8b8d608583b29bbdf1b5c79 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 15 Nov 2015 18:15:06 +0100 Subject: [PATCH 35/35] move ported middlewares to /api-v2 --- website/src/middlewares/{ => api-v2}/domain.js | 0 website/src/middlewares/{ => api-v2}/errorHandler.js | 0 website/src/server.js | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename website/src/middlewares/{ => api-v2}/domain.js (100%) rename website/src/middlewares/{ => api-v2}/errorHandler.js (100%) diff --git a/website/src/middlewares/domain.js b/website/src/middlewares/api-v2/domain.js similarity index 100% rename from website/src/middlewares/domain.js rename to website/src/middlewares/api-v2/domain.js diff --git a/website/src/middlewares/errorHandler.js b/website/src/middlewares/api-v2/errorHandler.js similarity index 100% rename from website/src/middlewares/errorHandler.js rename to website/src/middlewares/api-v2/errorHandler.js diff --git a/website/src/server.js b/website/src/server.js index 2e39f50d05..6573bb3c6e 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -97,7 +97,7 @@ attachMiddlewares(newApp); /* OLD APP IS DISABLED UNTIL COMPATIBLE WITH NEW MODELS //require('./middlewares/apiThrottle')(oldApp); -oldApp.use(require('./middlewares/domain')(server,mongoose)); +oldApp.use(require('./middlewares/api-v2/domain')(server,mongoose)); if (!IS_PROD && !DISABLE_LOGGING) oldApp.use(require('morgan')("dev")); oldApp.use(require('compression')()); oldApp.set("views", __dirname + "/../views"); @@ -155,7 +155,7 @@ oldApp.use('/common/script/public', express['static'](publicDir + "/../../common oldApp.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge })); oldApp.use(express['static'](publicDir)); -oldApp.use(require('./middlewares/errorHandler')); +oldApp.use(require('./middlewares/api-v2/errorHandler')); */ server.on('request', app);