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/Gruntfile.js b/Gruntfile.js index 7d8ea5a997..40752c9d90 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -130,7 +130,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" + diff --git a/package.json b/package.json index 81c577c43b..f13295e9e6 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "babel-core": "^5.8.34", "babelify": "^6.x.x", "body-parser": "^1.14.1", - "compression": "^1.6.0", "bower": "~1.3.12", "browserify": "~12.0.1", + "compression": "^1.6.0", "connect-ratelimit": "0.0.7", "cookie-parser": "^1.4.0", "cookie-session": "^1.2.0", @@ -63,7 +63,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..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/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/test/api/v3/unit/libs/buildManifest.test.js b/test/api/v3/unit/libs/buildManifest.test.js new file mode 100644 index 0000000000..d03e19c9eb --- /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.startsWith(' { + let getManifestFilesFn = () => { getManifestFiles('strange name here') }; + expect(getManifestFilesFn).to.throw(Error); + }); + }); +}); 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..353ac0ad09 --- /dev/null +++ b/test/api/v3/unit/libs/email.test.js @@ -0,0 +1,217 @@ +import request from 'request'; +import nconf from 'nconf'; +import nodemailer from 'nodemailer'; +import Q from 'q'; +import logger from '../../../../../website/src/libs/api-v3/logger'; + +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', () => { + let pathToEmailLib = '../../../../../website/src/libs/api-v3/email'; + + beforeEach(() => { + delete require.cache[require.resolve(pathToEmailLib)]; + }); + + 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']); + + 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 attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; + 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 attachEmail = require(pathToEmailLib); + let getUserInfo = attachEmail.getUserInfo; + 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', () => { + 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'), + } + } + })); + }); + }); +}); 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); + }); +}); 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..5bafc201dc --- /dev/null +++ b/test/api/v3/unit/libs/i18n.test.js @@ -0,0 +1,46 @@ +import { + translations, + localePath, + langCodes, +} from '../../../../../website/src/libs/api-v3/i18n'; +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('includes a translation object for each locale', () => { + listOfLocales.forEach((locale) => { + expect(translations[locale]).to.be.an('object'); + }); + }); + }); + + describe('localePath', () => { + it('is an absolute path to common/locales/', () => { + expect(localePath).to.match(/.*\/common\/locales\//); + expect(localePath) + }); + }); + + describe('langCodes', () => { + it('is a list of all the language codes', () => { + expect(langCodes.sort()).to.eql(listOfLocales); + }); + }); +}); 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..2cbf1d2c87 --- /dev/null +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -0,0 +1,241 @@ +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(); + }); + + context('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'); + }); + + 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', () => { + it('uses the user preferred language if avalaible', () => { + req.locals = { + user: { + preferences: { + language: 'it', + }, + }, + }; + + getUserLanguage(req, res, next); + expect(req.language).to.equal('it'); + }); + + it('falls back to english if the user preferred language is not avalaible', (done) => { + req.locals = { + user: { + preferences: { + language: 'bla', + }, + }, + }; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('en'); + 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'); + }); + }); + + context('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 + }; + + getUserLanguage(req, res, () => { + expect(req.language).to.equal('it'); + done(); + }); + }); + }); + + 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/test/common/algos.mocha.coffee b/test/common/algos.mocha.coffee new file mode 100644 index 0000000000..e914ab9194 --- /dev/null +++ b/test/common/algos.mocha.coffee @@ -0,0 +1,925 @@ +_ = require 'lodash' +expect = require 'expect.js' +sinon = require 'sinon' +moment = require 'moment' +shared = require '../../common/script/index.js' +shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations +test_helper = require './test_helper' +test_helper.addCustomMatchers() +$w = (s)->s.split(' ') + +### Helper Functions #### +newUser = (addTasks=true)-> + buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false} + user = + auth: + timestamps: {} + stats: {str:1, con:1, per:1, int:1, mp: 32, class: 'warrior', buffs: buffs} + items: + lastDrop: + count: 0 + hatchingPotions: {} + eggs: {} + food: {} + gear: + equipped: {} + costume: {} + owned: {} + quests: {} + party: + quest: + progress: + down: 0 + preferences: { + autoEquip: true + } + dailys: [] + todos: [] + rewards: [] + flags: {} + achievements: + ultimateGearSets: {} + contributor: + level: 2 + _tmp: {} + + shared.wrap(user) + user.ops.reset(null, ->) + if addTasks + _.each ['habit', 'todo', 'daily'], (task)-> + user.ops.addTask {body: {type: task, id: shared.uuid()}} + user + +rewrapUser = (user)-> + user._wrapped = false + shared.wrap(user) + user + +expectStrings = (obj, paths) -> + _.each paths, (path) -> expect(obj[path]).to.be.ok() + +# options.daysAgo: days ago when the last cron was executed +# cronAfterStart: moves the lastCron to be after the dayStart. +# This way the daysAgo works as expected if the test case +# makes the assumption that the lastCron was after dayStart. +beforeAfter = (options={}) -> + user = newUser() + [before, after] = [user, _.cloneDeep(user)] + # avoid closure on the original user + rewrapUser(after) + before.preferences.dayStart = after.preferences.dayStart = options.dayStart if options.dayStart + before.preferences.timezoneOffset = after.preferences.timezoneOffset = (options.timezoneOffset or moment().zone()) + if options.limitOne + before["#{options.limitOne}s"] = [before["#{options.limitOne}s"][0]] + after["#{options.limitOne}s"] = [after["#{options.limitOne}s"][0]] + lastCron = moment(options.now || +new Date).subtract( {days:options.daysAgo} ) if options.daysAgo + lastCron.add( {hours:options.dayStart, minutes:1} ) if options.daysAgo and options.cronAfterStart + lastCron = +lastCron if options.daysAgo + _.each [before,after], (obj) -> + obj.lastCron = lastCron if options.daysAgo + {before:before, after:after} +#TODO calculate actual points + +expectLostPoints = (before, after, taskType) -> + if taskType in ['daily','habit'] + expect(after.stats.hp).to.be.lessThan before.stats.hp + expect(after["#{taskType}s"][0].history).to.have.length(1) + else expect(after.history.todos).to.have.length(1) + expect(after).toHaveExp 0 + expect(after).toHaveGP 0 + expect(after["#{taskType}s"][0].value).to.be.lessThan before["#{taskType}s"][0].value + +expectGainedPoints = (before, after, taskType) -> + expect(after.stats.hp).to.be 50 + expect(after.stats.exp).to.be.greaterThan before.stats.exp + expect(after.stats.gp).to.be.greaterThan before.stats.gp + expect(after["#{taskType}s"][0].value).to.be.greaterThan before["#{taskType}s"][0].value + expect(after["#{taskType}s"][0].history).to.have.length(1) if taskType is 'habit' + # daily & todo histories handled on cron + +expectNoChange = (before,after) -> + _.each $w('stats items gear dailys todos rewards preferences'), (attr)-> + expect(after[attr]).to.eql before[attr] + +expectClosePoints = (before, after, taskType) -> + expect( Math.abs(after.stats.exp - before.stats.exp) ).to.be.lessThan 0.0001 + expect( Math.abs(after.stats.gp - before.stats.gp) ).to.be.lessThan 0.0001 + expect( Math.abs(after["#{taskType}s"][0].value - before["#{taskType}s"][0].value) ).to.be.lessThan 0.0001 + +expectDayResetNoDamage = (b,a) -> + [before,after] = [_.cloneDeep(b), _.cloneDeep(a)] + _.each after.dailys, (task,i) -> + expect(task.completed).to.be false + expect(before.dailys[i].value).to.be task.value + expect(before.dailys[i].streak).to.be task.streak + expect(task.history).to.have.length(1) + _.each after.todos, (task,i) -> + expect(task.completed).to.be false + expect(before.todos[i].value).to.be.greaterThan task.value + expect(after.history.todos).to.have.length(1) + # hack so we can compare user before/after obj equality sans effected paths + _.each [before,after], (obj) -> + delete obj.stats.buffs + _.each $w('dailys todos history lastCron'), (path) -> delete obj[path] + delete after._tmp + expectNoChange(before, after) + +cycle = (array)-> + n = -1 + (seed=0)-> + n++ + return array[n % array.length] + +repeatWithoutLastWeekday = ()-> + repeat = {su:true,m:true,t:true,w:true,th:true,f:true,s:true} + if shared.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday + repeat.su = false + else + repeat.s = false + {repeat: repeat} + +###### Specs ###### + +describe 'User', -> + it 'sets correct user defaults', -> + user = newUser() + base_gear = { armor: 'armor_base_0', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' } + buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false} + expect(user.stats).to.eql { str: 1, con: 1, per: 1, int: 1, hp: 50, mp: 32, lvl: 1, exp: 0, gp: 0, class: 'warrior', buffs: buffs } + expect(user.items.gear).to.eql { equipped: base_gear, costume: base_gear, owned: {weapon_warrior_0: true} } + expect(user.preferences).to.eql { autoEquip: true, costume: false } + + it 'calculates max MP', -> + user = newUser() + expect(user).toHaveMaxMP 32 + user.stats.int = 10 + expect(user).toHaveMaxMP 50 + user.stats.lvl = 5 + expect(user).toHaveMaxMP 54 + user.stats.class = 'wizard' + user.items.gear.equipped.weapon = 'weapon_wizard_1' + expect(user).toHaveMaxMP 63 + + it 'handles perfect days', -> + user = newUser() + user.dailys = [] + _.times 3, ->user.dailys.push shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days')}) + cron = -> user.lastCron = moment().subtract(1,'days');user.fns.cron() + + cron() + expect(user.stats.buffs.str).to.be 0 + expect(user.achievements.perfect).to.not.be.ok() + + user.dailys[0].completed = true + cron() + expect(user.stats.buffs.str).to.be 0 + expect(user.achievements.perfect).to.not.be.ok() + + _.each user.dailys, (d)->d.completed = true + cron() + expect(user.stats.buffs.str).to.be 1 + expect(user.achievements.perfect).to.be 1 + + # Handle greyed-out dailys + yesterday = moment().subtract(1,'days') + user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false + _.each user.dailys[1..], (d)->d.completed = true + cron() + expect(user.stats.buffs.str).to.be 1 + expect(user.achievements.perfect).to.be 2 + + describe 'Resting in the Inn', -> + user = null + cron = null + + beforeEach -> + user = newUser() + user.preferences.sleep = true + cron = -> user.lastCron = moment().subtract(1, 'days');user.fns.cron() + user.dailys = [] + _.times 2, -> user.dailys.push shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days')}) + + it 'remains in the inn on cron', -> + cron() + expect(user.preferences.sleep).to.be true + + it 'resets dailies', -> + user.dailys[0].completed = true + cron() + expect(user.dailys[0].completed).to.be false + + it 'resets checklist on incomplete dailies', -> + user.dailys[0].checklist = [ + { + "text" : "1", + "id" : "checklist-one", + "completed" : true + }, + { + "text" : "2", + "id" : "checklist-two", + "completed" : true + }, + { + "text" : "3", + "id" : "checklist-three", + "completed" : false + } + ] + cron() + _.each user.dailys[0].checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklist on complete dailies', -> + user.dailys[0].checklist = [ + { + "text" : "1", + "id" : "checklist-one", + "completed" : true + }, + { + "text" : "2", + "id" : "checklist-two", + "completed" : true + }, + { + "text" : "3", + "id" : "checklist-three", + "completed" : false + } + ] + user.dailys[0].completed = true + cron() + _.each user.dailys[0].checklist, (box)-> + expect(box.completed).to.be false + + it 'does not reset checklist on grey incomplete dailies', -> + yesterday = moment().subtract(1,'days') + user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false + user.dailys[0].checklist = [ + { + "text" : "1", + "id" : "checklist-one", + "completed" : true + }, + { + "text" : "2", + "id" : "checklist-two", + "completed" : true + }, + { + "text" : "3", + "id" : "checklist-three", + "completed" : true + } + ] + + cron() + _.each user.dailys[0].checklist, (box)-> + expect(box.completed).to.be true + + it 'resets checklist on complete grey complete dailies', -> + yesterday = moment().subtract(1,'days') + user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false + user.dailys[0].checklist = [ + { + "text" : "1", + "id" : "checklist-one", + "completed" : true + }, + { + "text" : "2", + "id" : "checklist-two", + "completed" : true + }, + { + "text" : "3", + "id" : "checklist-three", + "completed" : true + } + ] + user.dailys[0].completed = true + + cron() + _.each user.dailys[0].checklist, (box)-> + expect(box.completed).to.be false + + it 'does not damage user for incomplete dailies', -> + expect(user).toHaveHP 50 + user.dailys[0].completed = true + user.dailys[1].completed = false + cron() + expect(user).toHaveHP 50 + + it 'gives credit for complete dailies', -> + user.dailys[0].completed = true + expect(user.dailys[0].history).to.be.empty + cron() + expect(user.dailys[0].history).to.not.be.empty + + it 'damages user for incomplete dailies after checkout', -> + expect(user).toHaveHP 50 + user.dailys[0].completed = true + user.dailys[1].completed = false + user.preferences.sleep = false + cron() + expect(user.stats.hp).to.be.lessThan 50 + + describe 'Death', -> + user = undefined + it 'revives correctly', -> + user = newUser() + user.stats = { gp: 10, exp: 100, lvl: 2, hp: 0, class: 'warrior' } + user.ops.revive() + expect(user).toHaveGP 0 + expect(user).toHaveExp 0 + expect(user).toHaveLevel 1 + expect(user).toHaveHP 50 + expect(user.items.gear.owned).to.eql { weapon_warrior_0: false } + + it "doesn't break unbreakables", -> + ce = shared.countExists + user = newUser() + # breakables (includes default weapon_warrior_0): + user.items.gear.owned['shield_warrior_1'] = true + # unbreakables because off-class or 0 value: + user.items.gear.owned['shield_rogue_1'] = true + user.items.gear.owned['head_special_nye'] = true + expect(ce user.items.gear.owned).to.be 4 + user.stats.hp = 0 + user.ops.revive() + expect(ce(user.items.gear.owned)).to.be 3 + user.stats.hp = 0 + user.ops.revive() + expect(ce(user.items.gear.owned)).to.be 2 + user.stats.hp = 0 + user.ops.revive() + expect(ce(user.items.gear.owned)).to.be 2 + expect(user.items.gear.owned).to.eql { weapon_warrior_0: false, shield_warrior_1: false, shield_rogue_1: true, head_special_nye: true } + + it "handles event items", -> + shared.content.gear.flat.head_special_nye.event.start = '2012-01-01' + shared.content.gear.flat.head_special_nye.event.end = '2012-02-01' + expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true + delete user.items.gear.owned['head_special_nye'] + expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be false + + shared.content.gear.flat.head_special_nye.event.start = moment().subtract(5,'days') + shared.content.gear.flat.head_special_nye.event.end = moment().add(5,'days') + expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true + + describe 'Rebirth', -> + user = undefined + it 'removes correct gear', -> + user = newUser() + user.stats.lvl = 100 + user.items.gear.owned = { + "weapon_warrior_0": true, + "weapon_warrior_1": true, + "armor_warrior_1": false, + "armor_mystery_201402": true, + "back_mystery_201402": false, + "head_mystery_201402": true, + "weapon_armoire_basicCrossbow": true, + } + user.ops.rebirth() + expect(user.items.gear.owned).to.eql { + "weapon_warrior_0": true, + "weapon_warrior_1": false, + "armor_warrior_1": false, + "armor_mystery_201402": true, + "back_mystery_201402": false, + "head_mystery_201402": true, + "weapon_armoire_basicCrossbow": false, + } + + describe 'store', -> + it 'buys a Quest scroll', -> + user = newUser() + user.stats.gp = 205 + user.ops.buyQuest {params: {key: 'dilatoryDistress1'}} + expect(user.items.quests).to.eql {dilatoryDistress1: 1} + expect(user).toHaveGP 5 + + it 'does not buy Quests without enough Gold', -> + user = newUser() + user.stats.gp = 1 + user.ops.buyQuest {params: {key: 'dilatoryDistress1'}} + expect(user.items.quests).to.eql {} + expect(user).toHaveGP 1 + + it 'does not buy nonexistent Quests', -> + user = newUser() + user.stats.gp = 9999 + user.ops.buyQuest {params: {key: 'snarfblatter'}} + expect(user.items.quests).to.eql {} + expect(user).toHaveGP 9999 + + it 'does not buy Gem-premium Quests', -> + user = newUser() + user.stats.gp = 9999 + user.ops.buyQuest {params: {key: 'kraken'}} + expect(user.items.quests).to.eql {} + expect(user).toHaveGP 9999 + + describe 'Gem purchases', -> + it 'does not purchase items without enough Gems', -> + user = newUser() + user.ops.purchase {params: {type: 'eggs', key: 'Cactus'}} + user.ops.purchase {params: {type: 'gear', key: 'headAccessory_special_foxEars'}} + user.ops.unlock {query: {path: 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars'}} + expect(user.items.eggs).to.eql {} + expect(user.items.gear.owned).to.eql { weapon_warrior_0: true } + + it 'purchases an egg', -> + user = newUser() + user.balance = 1 + user.ops.purchase {params: {type: 'eggs', key: 'Cactus'}} + expect(user.items.eggs).to.eql { Cactus: 1} + expect(user.balance).to.eql 0.25 + + it 'purchases fox ears', -> + user = newUser() + user.balance = 1 + user.ops.purchase {params: {type: 'gear', key: 'headAccessory_special_foxEars'}} + expect(user.items.gear.owned).to.eql { weapon_warrior_0: true, headAccessory_special_foxEars: true } + expect(user.balance).to.eql 0.5 + + it 'unlocks all the animal ears at once', -> + user = newUser() + user.balance = 2 + user.ops.unlock {query: {path: 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars'}} + expect(user.items.gear.owned).to.eql { weapon_warrior_0: true, headAccessory_special_bearEars: true, headAccessory_special_cactusEars: true, headAccessory_special_foxEars: true, headAccessory_special_lionEars: true, headAccessory_special_pandaEars: true, headAccessory_special_pigEars: true, headAccessory_special_tigerEars: true, headAccessory_special_wolfEars: true} + expect(user.balance).to.eql 0.75 + + describe 'spells', -> + _.each shared.content.spells, (spellClass)-> + _.each spellClass, (spell)-> + it "#{spell.text} has valid values", -> + expect(spell.target).to.match(/^(task|self|party|user)$/) + expect(spell.mana).to.be.an('number') + if spell.lvl + expect(spell.lvl).to.be.an('number') + expect(spell.lvl).to.be.above(0) + expect(spell.cast).to.be.a('function') + + describe 'drop system', -> + user = null + MIN_RANGE_FOR_POTION = 0 + MAX_RANGE_FOR_POTION = .3 + MIN_RANGE_FOR_EGG = .4 + MAX_RANGE_FOR_EGG = .6 + MIN_RANGE_FOR_FOOD = .7 + MAX_RANGE_FOR_FOOD = 1 + + beforeEach -> + user = newUser() + user.flags.dropsEnabled = true + @task_id = shared.uuid() + user.ops.addTask({body: {type: 'daily', id: @task_id}}) + + it 'drops a hatching potion', -> + for random in [MIN_RANGE_FOR_POTION..MAX_RANGE_FOR_POTION] by .1 + sinon.stub(user.fns, 'predictableRandom').returns random + user.ops.score {params: { id: @task_id, direction: 'up'}} + expect(user.items.eggs).to.be.empty + expect(user.items.hatchingPotions).to.not.be.empty + expect(user.items.food).to.be.empty + user.fns.predictableRandom.restore() + + it 'drops a pet egg', -> + for random in [MIN_RANGE_FOR_EGG..MAX_RANGE_FOR_EGG] by .1 + sinon.stub(user.fns, 'predictableRandom').returns random + user.ops.score {params: { id: @task_id, direction: 'up'}} + expect(user.items.eggs).to.not.be.empty + expect(user.items.hatchingPotions).to.be.empty + expect(user.items.food).to.be.empty + user.fns.predictableRandom.restore() + + it 'drops food', -> + for random in [MIN_RANGE_FOR_FOOD..MAX_RANGE_FOR_FOOD] by .1 + sinon.stub(user.fns, 'predictableRandom').returns random + user.ops.score {params: { id: @task_id, direction: 'up'}} + expect(user.items.eggs).to.be.empty + expect(user.items.hatchingPotions).to.be.empty + expect(user.items.food).to.not.be.empty + user.fns.predictableRandom.restore() + + it 'does not get a drop', -> + sinon.stub(user.fns, 'predictableRandom').returns 0.5 + user.ops.score {params: { id: @task_id, direction: 'up'}} + expect(user.items.eggs).to.eql {} + expect(user.items.hatchingPotions).to.eql {} + expect(user.items.food).to.eql {} + user.fns.predictableRandom.restore() + + describe 'Quests', -> + _.each shared.content.quests, (quest)-> + it "#{quest.text()} has valid values", -> + expect(quest.notes()).to.be.an('string') + expect(quest.completion()).to.be.an('string') if quest.completion + expect(quest.previous).to.be.an('string') if quest.previous + expect(quest.value).to.be.greaterThan 0 if quest.canBuy() + expect(quest.drop.gp).to.not.be.lessThan 0 + expect(quest.drop.exp).to.not.be.lessThan 0 + expect(quest.category).to.match(/pet|unlockable|gold|world/) + if quest.drop.items + expect(quest.drop.items).to.be.an(Array) + if quest.boss + expect(quest.boss.name()).to.be.an('string') + expect(quest.boss.hp).to.be.greaterThan 0 + expect(quest.boss.str).to.be.greaterThan 0 + else if quest.collect + _.each quest.collect, (collect)-> + expect(collect.text()).to.be.an('string') + expect(collect.count).to.be.greaterThan 0 + + describe 'Achievements', -> + _.each shared.content.classes, (klass) -> + user = newUser() + user.stats.gp = 10000 + _.each shared.content.gearTypes, (type) -> + _.each [1..5], (i) -> + user.ops.buy {params:'#{type}_#{klass}_#{i}'} + it 'does not get ultimateGear ' + klass, -> + expect(user.achievements.ultimateGearSets[klass]).to.not.be.ok() + _.each shared.content.gearTypes, (type) -> + user.ops.buy {params:'#{type}_#{klass}_6'} + xit 'gets ultimateGear ' + klass, -> + expect(user.achievements.ultimateGearSets[klass]).to.be.ok() + + it 'does not remove existing Ultimate Gear achievements', -> + user = newUser() + user.achievements.ultimateGearSets = {'healer':true,'wizard':true,'rogue':true,'warrior':true} + user.items.gear.owned.shield_warrior_5 = false + user.items.gear.owned.weapon_rogue_6 = false + user.ops.buy {params:'shield_warrior_5'} + expect(user.achievements.ultimateGearSets).to.eql {'healer':true,'wizard':true,'rogue':true,'warrior':true} + + describe 'unlocking features', -> + it 'unlocks drops at level 3', -> + user = newUser() + user.stats.lvl = 3 + user.fns.updateStats(user.stats) + expect(user.flags.dropsEnabled).to.be.ok() + + it 'unlocks Rebirth at level 50', -> + user = newUser() + user.stats.lvl = 50 + user.fns.updateStats(user.stats) + expect(user.flags.rebirthEnabled).to.be.ok() + + describe 'level-awarded Quests', -> + it 'gets Attack of the Mundane at level 15', -> + user = newUser() + user.stats.lvl = 15 + user.fns.updateStats(user.stats) + expect(user.flags.levelDrops.atom1).to.be.ok() + expect(user.items.quests.atom1).to.eql 1 + + it 'gets Vice at level 30', -> + user = newUser() + user.stats.lvl = 30 + user.fns.updateStats(user.stats) + expect(user.flags.levelDrops.vice1).to.be.ok() + expect(user.items.quests.vice1).to.eql 1 + + it 'gets Golden Knight at level 40', -> + user = newUser() + user.stats.lvl = 40 + user.fns.updateStats(user.stats) + expect(user.flags.levelDrops.goldenknight1).to.be.ok() + expect(user.items.quests.goldenknight1).to.eql 1 + + it 'gets Moonstone Chain at level 60', -> + user = newUser() + user.stats.lvl = 60 + user.fns.updateStats(user.stats) + expect(user.flags.levelDrops.moonstone1).to.be.ok() + expect(user.items.quests.moonstone1).to.eql 1 + +describe 'Simple Scoring', -> + beforeEach -> + {@before, @after} = beforeAfter() + + it 'Habits : Up', -> + @after.ops.score {params: {id: @after.habits[0].id, direction: 'down'}, query: {times: 5}} + expectLostPoints(@before, @after,'habit') + + it 'Habits : Down', -> + @after.ops.score {params: {id: @after.habits[0].id, direction: 'up'}, query: {times: 5}} + expectGainedPoints(@before, @after,'habit') + + it 'Dailys : Up', -> + @after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}} + expectGainedPoints(@before, @after,'daily') + + it 'Dailys : Up, Down', -> + @after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}} + @after.ops.score {params: {id: @after.dailys[0].id, direction: 'down'}} + expectClosePoints(@before, @after, 'daily') + + it 'Todos : Up', -> + @after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}} + expectGainedPoints(@before, @after,'todo') + + it 'Todos : Up, Down', -> + @after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}} + @after.ops.score {params: {id: @after.todos[0].id, direction: 'down'}} + expectClosePoints(@before, @after, 'todo') + +describe 'Cron', -> + + it 'computes shouldCron', -> + user = newUser() + + paths = {};user.fns.cron {paths} + expect(user.lastCron).to.not.be.ok # it setup the cron property now + + user.lastCron = +moment().subtract(1,'days') + + paths = {};user.fns.cron {paths} + expect(user.lastCron).to.be.greaterThan 0 + +# user.lastCron = +moment().add(1,'days') +# paths = {};algos.cron user, {paths} +# expect(paths.lastCron).to.be true # busted cron (was set to after today's date) + + it 'only dailies & todos are affected', -> + {before,after} = beforeAfter({daysAgo:1}) + before.dailys = before.todos = after.dailys = after.todos = [] + after.fns.cron() + before.stats.mp=after.stats.mp #FIXME + expect(after.lastCron).to.not.be before.lastCron # make sure cron was run + delete after.stats.buffs;delete before.stats.buffs + expect(before.stats).to.eql after.stats + beforeTasks = before.habits.concat(before.dailys).concat(before.todos).concat(before.rewards) + afterTasks = after.habits.concat(after.dailys).concat(after.todos).concat(after.rewards) + expect(beforeTasks).to.eql afterTasks + + describe 'preening', -> + beforeEach -> + @clock = sinon.useFakeTimers(Date.parse("2013-11-20"), "Date") + + afterEach -> + @clock.restore() + + it 'should preen user history', -> + {before,after} = beforeAfter({daysAgo:1}) + history = [ + # Last year should be condensed to one entry, avg: 1 + {date:'09/01/2012', value: 0} + {date:'10/01/2012', value: 0} + {date:'11/01/2012', value: 2} + {date:'12/01/2012', value: 2} + + # Each month of this year should be condensed to 1/mo, averages follow + {date:'01/01/2013', value: 1} #2 + {date:'01/15/2013', value: 3} + + {date:'02/01/2013', value: 2} #3 + {date:'02/15/2013', value: 4} + + {date:'03/01/2013', value: 3} #4 + {date:'03/15/2013', value: 5} + + {date:'04/01/2013', value: 4} #5 + {date:'04/15/2013', value: 6} + + {date:'05/01/2013', value: 5} #6 + {date:'05/15/2013', value: 7} + + {date:'06/01/2013', value: 6} #7 + {date:'06/15/2013', value: 8} + + {date:'07/01/2013', value: 7} #8 + {date:'07/15/2013', value: 9} + + {date:'08/01/2013', value: 8} #9 + {date:'08/15/2013', value: 10} + + {date:'09/01/2013', value: 9} #10 + {date:'09/15/2013', value: 11} + + {date:'010/01/2013', value: 10} #11 + {date:'010/15/2013', value: 12} + + # This month should condense each week + {date:'011/01/2013', value: 12} + {date:'011/02/2013', value: 13} + {date:'011/03/2013', value: 14} + {date:'011/04/2013', value: 15} + ] + after.history = {exp: _.cloneDeep(history), todos: _.cloneDeep(history)} + after.habits[0].history = _.cloneDeep(history) + after.fns.cron() + + # remove history entries created by cron + after.history.exp.pop() + after.history.todos.pop() + + _.each [after.history.exp, after.history.todos, after.habits[0].history], (arr) -> + expect(_.map(arr, (x)->x.value)).to.eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + + describe 'Todos', -> + it '1 day missed', -> + {before,after} = beforeAfter({daysAgo:1}) + before.dailys = after.dailys = [] + after.fns.cron() + + # todos don't effect stats + expect(after).toHaveHP 50 + expect(after).toHaveExp 0 + expect(after).toHaveGP 0 + + # but they devalue + expect(before.todos[0].value).to.be 0 # sanity check for task setup + expect(after.todos[0].value).to.be -1 # the actual test + expect(after.history.todos).to.have.length 1 + + it '2 days missed', -> + {before,after} = beforeAfter({daysAgo:2}) + before.dailys = after.dailys = [] + after.fns.cron() + + # todos devalue by only one day's worth of devaluation + expect(before.todos[0].value).to.be 0 # sanity check for task setup + expect(after.todos[0].value).to.be -1 # the actual test + + # I used hard-coded dates here instead of 'now' so the tests don't fail + # when you run them between midnight and dayStart. Nothing worse than + # intermittent failures. + describe 'cron day calculations', -> + dayStart = 4 + fstr = "YYYY-MM-DD HH:mm:ss" + + it 'startOfDay before dayStart', -> + # If the time is before dayStart, then we expect the start of the day to be yesterday at dayStart + start = shared.startOfDay {now: moment('2014-10-09 02:30:00'), dayStart} + expect(start.format(fstr)).to.eql '2014-10-08 04:00:00' + + it 'startOfDay after dayStart', -> + # If the time is after dayStart, then we expect the start of the day to be today at dayStart + start = shared.startOfDay {now: moment('2014-10-09 05:30:00'), dayStart} + expect(start.format(fstr)).to.eql '2014-10-09 04:00:00' + + it 'daysSince cron before, now after', -> + # If the lastCron was before dayStart, then a time on the same day after dayStart + # should be 1 day later than lastCron + lastCron = moment('2014-10-09 02:30:00') + days = shared.daysSince(lastCron, {now: moment('2014-10-09 11:30:00'), dayStart}) + expect(days).to.eql 1 + + it 'daysSince cron before, now before', -> + # If the lastCron was before dayStart, then a time on the same day also before dayStart + # should be 0 days later than lastCron + lastCron = moment('2014-10-09 02:30:00') + days = shared.daysSince(lastCron, {now: moment('2014-10-09 03:30:00'), dayStart}) + expect(days).to.eql 0 + + it 'daysSince cron after, now after', -> + # If the lastCron was after dayStart, then a time on the same day also after dayStart + # should be 0 days later than lastCron + lastCron = moment('2014-10-09 05:30:00') + days = shared.daysSince(lastCron, {now: moment('2014-10-09 06:30:00'), dayStart}) + expect(days).to.eql 0 + + it 'daysSince cron after, now tomorrow before', -> + # If the lastCron was after dayStart, then a time on the following day but before dayStart + # should be 0 days later than lastCron + lastCron = moment('2014-10-09 12:30:00') + days = shared.daysSince(lastCron, {now: moment('2014-10-10 01:30:00'), dayStart}) + expect(days).to.eql 0 + + it 'daysSince cron after, now tomorrow after', -> + # If the lastCron was after dayStart, then a time on the following day and after dayStart + # should be 1 day later than lastCron + lastCron = moment('2014-10-09 12:30:00') + days = shared.daysSince(lastCron, {now: moment('2014-10-10 10:30:00'), dayStart}) + expect(days).to.eql 1 + + xit 'daysSince, last cron before new dayStart', -> + # If lastCron was after dayStart (at 1am) with dayStart set at 0, changing dayStart to 4am + # should not trigger another cron the same day + + # dayStart is 0 + lastCron = moment('2014-10-09 01:00:00') + # dayStart is 4 + days = shared.daysSince(lastCron, {now: moment('2014-10-09 05:00:00'), dayStart}) + expect(days).to.eql 0 + + describe 'dailies', -> + + describe 'new day', -> + + ### + This section runs through a "cron matrix" of all permutations (that I can easily account for). It sets + task due days, user custom day start, timezoneOffset, etc - then runs cron, jumps to tomorrow and runs cron, + and so on - testing each possible outcome along the way + ### + + runCron = (options) -> + _.each [480, 240, 0, -120], (timezoneOffset) -> # test different timezones + now = shared.startOfWeek({timezoneOffset}).add(options.currentHour||0, 'hours') + {before,after} = beforeAfter({now, timezoneOffset, daysAgo:1, cronAfterStart:options.cronAfterStart||true, dayStart:options.dayStart||0, limitOne:'daily'}) + before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat + before.dailys[0].streak = after.dailys[0].streak = 10 + before.dailys[0].completed = after.dailys[0].completed = true if options.checked + before.dailys[0].startDate = after.dailys[0].startDate = moment().subtract(30, 'days') + if options.shouldDo + expect(shared.shouldDo(now.toDate(), after.dailys[0], {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok() + after.fns.cron {now} + before.stats.mp=after.stats.mp #FIXME + switch options.expect + when 'losePoints' then expectLostPoints(before,after,'daily') + when 'noChange' then expectNoChange(before,after) + when 'noDamage' then expectDayResetNoDamage(before,after) + {before,after} + + # These test cases were written assuming that lastCron was run after dayStart + # even if currentHour < dayStart and lastCron = yesterday at currentHour. + # cronAfterStart makes sure that lastCron is moved to be after dayStart. + cronMatrix = + steps: + + 'due yesterday': + defaults: {daysAgo:1, cronAfterStart:true, limitOne: 'daily'} + steps: + + '(simple)': {expect:'losePoints'} + + 'due today': + # NOTE: a strange thing here, moment().startOf('week') is Sunday, but moment.zone(myTimeZone).startOf('week') is Monday. + defaults: {repeat:{su:true,m:true,t:true,w:true,th:true,f:true,s:true}} + steps: + 'pre-dayStart': + defaults: {currentHour:3, dayStart:4, shouldDo:true} + steps: + 'checked': {checked: true, expect:'noChange'} + 'un-checked': {checked: false, expect:'noChange'} + 'post-dayStart': + defaults: {currentHour:5, dayStart:4, shouldDo:true} + steps: + 'checked': {checked:true, expect:'noDamage'} + 'unchecked': {checked:false, expect: 'losePoints'} + + 'NOT due today': + defaults: {repeat:{su:true,m:false,t:true,w:true,th:true,f:true,s:true}} + steps: + 'pre-dayStart': + defaults: {currentHour:3, dayStart:4, shouldDo:true} + steps: + 'checked': {checked: true, expect:'noChange'} + 'un-checked': {checked: false, expect:'noChange'} + 'post-dayStart': + defaults: {currentHour:5, dayStart:4, shouldDo:false} + steps: + 'checked': {checked:true, expect:'noDamage'} + 'unchecked': {checked:false, expect: 'losePoints'} + + 'not due yesterday': + defaults: repeatWithoutLastWeekday() + steps: + '(simple)': {expect:'noDamage'} + 'post-dayStart': {currentHour:5,dayStart:4, expect:'noDamage'} + 'pre-dayStart': {currentHour:3, dayStart:4, expect:'noChange'} + + recurseCronMatrix = (obj, options={}) -> + if obj.steps + _.each obj.steps, (step, text) -> + o = _.cloneDeep options + o.text ?= ''; o.text += " #{text} " + recurseCronMatrix step, _.defaults(o,obj.defaults) + else + it "#{options.text}", -> runCron(_.defaults(obj,options)) + recurseCronMatrix(cronMatrix) + +describe 'Helper', -> + + it 'calculates gold coins', -> + expect(shared.gold(10)).to.eql 10 + expect(shared.gold(1.957)).to.eql 1 + expect(shared.gold()).to.eql 0 + + it 'calculates silver coins', -> + expect(shared.silver(10)).to.eql 0 + expect(shared.silver(1.957)).to.eql 95 + expect(shared.silver(0.01)).to.eql "01" + expect(shared.silver()).to.eql "00" + + it 'calculates experience to next level', -> + expect(shared.tnl 1).to.eql 150 + expect(shared.tnl 2).to.eql 160 + expect(shared.tnl 10).to.eql 260 + expect(shared.tnl 99).to.eql 3580 + + it 'calculates the start of the day', -> + fstr = 'YYYY-MM-DD HH:mm:ss' + today = '2013-01-01 00:00:00' + # get the timezone for the day, so the test case doesn't fail + # if you run it during daylight savings time because by default + # it uses moment().zone() which is the current minute offset + zone = moment(today).zone() + expect(shared.startOfDay({now: new Date(2013, 0, 1, 0)}, timezoneOffset:zone).format(fstr)).to.eql today + expect(shared.startOfDay({now: new Date(2013, 0, 1, 5)}, timezoneOffset:zone).format(fstr)).to.eql today + expect(shared.startOfDay({now: new Date(2013, 0, 1, 23, 59, 59), timezoneOffset:zone}).format(fstr)).to.eql today diff --git a/test/common/algos.mocha.js b/test/common/algos.mocha.js index 06ebd3044d..6a568c115c 100644 --- a/test/common/algos.mocha.js +++ b/test/common/algos.mocha.js @@ -10,7 +10,7 @@ moment = require('moment'); shared = require('../../common/script/index.js'); -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'); diff --git a/test/common/dailies.coffee b/test/common/dailies.coffee new file mode 100644 index 0000000000..da00c88871 --- /dev/null +++ b/test/common/dailies.coffee @@ -0,0 +1,418 @@ +_ = require 'lodash' +expect = require 'expect.js' +sinon = require 'sinon' +moment = require 'moment' +shared = require '../../common/script/index.js' +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} + if shared.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday + repeat.su = false + else + repeat.s = false + {repeat: repeat} + +### Helper Functions #### +# @TODO: Refactor into helper file +newUser = (addTasks=true)-> + buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false} + user = + auth: + timestamps: {} + stats: {str:1, con:1, per:1, int:1, mp: 32, class: 'warrior', buffs: buffs} + items: + lastDrop: + count: 0 + hatchingPotions: {} + eggs: {} + food: {} + gear: + equipped: {} + costume: {} + party: + quest: + progress: + down: 0 + preferences: {} + dailys: [] + todos: [] + rewards: [] + flags: {} + achievements: {} + contributor: + level: 2 + shared.wrap(user) + user.ops.reset(null, ->) + if addTasks + _.each ['habit', 'todo', 'daily'], (task)-> + user.ops.addTask {body: {type: task, id: shared.uuid()}} + user + +cron = (usr, missedDays=1) -> + usr.lastCron = moment().subtract(missedDays,'days') + usr.fns.cron() + +describe 'daily/weekly that repeats everyday (default)', -> + user = null + daily = null + weekly = null + + describe 'when startDate is in the future', -> + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().add(7, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().add(7, 'days'), frequency: 'weekly', repeat: {su:true,m:true,t:true,w:true,th:true,f:true,s:true}}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does not damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be 50 + + it 'does not change value on cron if daily is incomplete', -> + cron(user) + expect(daily.value).to.be 0 + expect(weekly.value).to.be 0 + + it 'does not reset checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + expect(daily.checklist[0].completed).to.be true + expect(daily.checklist[1].completed).to.be true + expect(daily.checklist[2].completed).to.be false + + expect(weekly.checklist[0].completed).to.be true + expect(weekly.checklist[1].completed).to.be true + expect(weekly.checklist[2].completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + daily.completed = true + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'is due on startDate', -> + daily_due_today = shared.shouldDo moment(), daily + daily_due_on_start_date = shared.shouldDo moment().add(7, 'days'), daily + + expect(daily_due_today).to.be false + expect(daily_due_on_start_date).to.be true + + weekly_due_today = shared.shouldDo moment(), weekly + weekly_due_on_start_date = shared.shouldDo moment().add(7, 'days'), weekly + + expect(weekly_due_today).to.be false + expect(weekly_due_on_start_date).to.be true + + describe 'when startDate is in the past', -> + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days'), frequency: 'weekly'}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be.lessThan 50 + + it 'decreases value on cron if daily is incomplete', -> + cron(user, 1) + expect(daily.value).to.be -1 + expect(weekly.value).to.be -1 + + it 'decreases value on cron once only if daily is incomplete and multiple days are missed', -> + cron(user, 7) + expect(daily.value).to.be -1 + expect(weekly.value).to.be -1 + + it 'resets checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + daily.completed = true + weekly.checklist = checklist + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + describe 'when startDate is today', -> + beforeEach -> + user = newUser() + user.dailys = [ + # Must set start date to yesterday, because cron mock sets last cron to yesterday + shared.taskDefaults({type:'daily', startDate: moment().subtract(1, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().subtract(1, 'days'), frequency: 'weekly'}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be.lessThan 50 + + it 'decreases value on cron if daily is incomplete', -> + cron(user) + expect(daily.value).to.be.lessThan 0 + expect(weekly.value).to.be.lessThan 0 + + it 'resets checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + daily.completed = true + weekly.checklist = checklist + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + +describe 'daily that repeats every x days', -> + user = null + daily = null + + beforeEach -> + user = newUser() + user.dailys = [ shared.taskDefaults({type:'daily', startDate: moment(), frequency: 'daily'}) ] + daily = user.dailys[0] + + _.times 11, (due) -> + + it 'where x equals ' + due, -> + daily.everyX = due + + _.times 30, (day) -> + isDue = shared.shouldDo moment().add(day, 'days'), daily + expect(isDue).to.be true if day % due == 0 + expect(isDue).to.be false if day % due != 0 + +describe 'daily that repeats every X days when multiple days are missed', -> + everyX = 3 + startDateDaysAgo = everyX * 3 + user = null + daily = null + + describe 'including missing a due date', -> + missedDays = everyX * 2 + 1 + + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().subtract(startDateDaysAgo, 'days'), frequency: 'daily', everyX: everyX}) + ] + daily = user.dailys[0] + + it 'decreases value on cron once only if daily is incomplete', -> + cron(user, missedDays) + expect(daily.value).to.be -1 + + it 'resets checklists if daily is incomplete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + } + ] + daily.checklist = checklist + cron(user, missedDays) + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + } + ] + daily.checklist = checklist + daily.completed = true + cron(user, missedDays) + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + describe 'but not missing a due date', -> + missedDays = everyX - 1 + + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().subtract(startDateDaysAgo, 'days'), frequency: 'daily', everyX: everyX}) + ] + daily = user.dailys[0] + + it 'does not decrease value on cron', -> + cron(user, missedDays) + expect(daily.value).to.be 0 + + it 'does not reset checklists if daily is incomplete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + } + ] + daily.checklist = checklist + cron(user, missedDays) + _.each daily.checklist, (box)-> + expect(box.completed).to.be true + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + } + ] + daily.checklist = checklist + daily.completed = true + cron(user, missedDays) + _.each daily.checklist, (box)-> + expect(box.completed).to.be false diff --git a/test/common/dailies.js b/test/common/dailies.js index 92d80ee5ad..6e8f1cfe40 100644 --- a/test/common/dailies.js +++ b/test/common/dailies.js @@ -10,7 +10,7 @@ moment = require('moment'); shared = require('../../common/script/index.js'); -shared.i18n.translations = require('../../website/src/libs/i18n.js').translations; +shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations; repeatWithoutLastWeekday = function() { var repeat; diff --git a/test/common/user.fns.ultimateGear.test.js b/test/common/user.fns.ultimateGear.test.js index 9dc80eaea5..17ab5a0684 100644 --- a/test/common/user.fns.ultimateGear.test.js +++ b/test/common/user.fns.ultimateGear.test.js @@ -1,5 +1,5 @@ var shared = require('../../common/script/index.js'); -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 3e1ae734cc..92e67294df 100644 --- a/test/helpers/api-integration.helper.js +++ b/test/helpers/api-integration.helper.js @@ -8,7 +8,7 @@ import {MongoClient as mongo} from 'mongodb'; import {v4 as generateUUID} from 'uuid'; import superagent from 'superagent'; import i18n from '../../common/script/src/i18n'; -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..62516f2751 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(); @@ -35,6 +35,7 @@ export function generateReq(options={}) { let defaultReq = { body: {}, query: {}, + headers: {}, }; return defaults(options, defaultReq); diff --git a/test/helpers/content.helper.js b/test/helpers/content.helper.js index db95a1f0b3..9c6b3fcdad 100644 --- a/test/helpers/content.helper.js +++ b/test/helpers/content.helper.js @@ -2,7 +2,7 @@ require('./globals.helper'); import {each} from 'lodash'; import i18n from '../../common/script/src/i18n'; -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/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/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; diff --git a/website/src/controllers/api-v2/auth.js b/website/src/controllers/api-v2/auth.js index 73a8a8e38d..dd379cc095 100644 --- a/website/src/controllers/api-v2/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -3,14 +3,14 @@ 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'); 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/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 0fc969c3af..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; @@ -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/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 cd747e7d5c..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; @@ -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/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 b1f47d8970..2440dde36f 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/api-v2/analytics.js b/website/src/libs/api-v2/analytics.js index dff60a358a..6c1ccd3020 100644 --- a/website/src/libs/api-v2/analytics.js +++ b/website/src/libs/api-v2/analytics.js @@ -1,7 +1,7 @@ -require('../i18n'); +require('./i18n'); var _ = require('lodash'); -var Content = require('../../../common').content; +var Content = require('../../../../common').content; var Amplitude = require('amplitude'); var googleAnalytics = require('universal-analytics'); 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/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/i18n.js b/website/src/libs/api-v2/i18n.js similarity index 96% rename from website/src/libs/i18n.js rename to website/src/libs/api-v2/i18n.js index 335b4d5789..e8295deb03 100644 --- a/website/src/libs/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)); 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/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/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/libs/api-v3/email.js b/website/src/libs/api-v3/email.js new file mode 100644 index 0000000000..65cbcce02a --- /dev/null +++ b/website/src/libs/api-v3/email.js @@ -0,0 +1,158 @@ +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) { + 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; + } + } + } + + 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) { + 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 sendTxn (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.post({ + url: `${EMAIL_SERVER.url}/job`, + 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..0f5f9d83dd --- /dev/null +++ b/website/src/libs/api-v3/encryption.js @@ -0,0 +1,25 @@ +import { + createCipher, + createDecipher, +} from 'crypto'; +import nconf from 'nconf'; + +// TODO check this is secure +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/libs/api-v3/firebase.js b/website/src/libs/api-v3/firebase.js new file mode 100644 index 0000000000..92d6c28fc1 --- /dev/null +++ b/website/src/libs/api-v3/firebase.js @@ -0,0 +1,67 @@ +import Firebase from 'firebase'; +import nconf from 'nconf'; +const FIREBASE_CONFIG = nconf.get('FIREBASE'); +const FIREBASE_ENABLED = 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/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js new file mode 100644 index 0000000000..ed2134f7bf --- /dev/null +++ b/website/src/libs/api-v3/i18n.js @@ -0,0 +1,118 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; +import shared from '../../../../common'; + +export 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_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 +// 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); +} 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; 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/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/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js new file mode 100644 index 0000000000..234a78a29c --- /dev/null +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -0,0 +1,79 @@ +import { model as User } from '../../models/user'; +import accepts from 'accepts'; +import _ from 'lodash'; +import { + translations, + defaultLangCodes, + multipleVersionsLanguages, +} from '../../libs/api-v3/i18n'; + +function _getUniqueListOfLanguages (languages) { + let acceptableLanguages = _(languages).map((lang) => { + return lang.slice(0, 2); + }).uniq().value(); + + let uniqueListOfLanguages = _.intersection(acceptableLanguages, defaultLangCodes); + + return uniqueListOfLanguages; +} + +function _checkForApplicableLanguageVariant (originalLanguageOptions) { + let languageVariant = _.find(originalLanguageOptions, (accepted) => { + let trimmedAccepted = accepted.slice(0, 2); + + return multipleVersionsLanguages[trimmedAccepted]; + }); + + 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'; + } + + return languageMapping[languageVariant] || baseLanguage; + } else { + return baseLanguage || 'en'; + } +} + +function _getFromUser (user, req) { + let preferredLang = user && user.preferences && user.preferences.language; + let lang = translations[preferredLang] ? preferredLang : _getFromBrowser(req); + + 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(); + } +} diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index 2f238a6a99..223186a81a 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -1,9 +1,9 @@ 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/i18n'); -var buildManifest = require('../libs/buildManifest'); +var i18n = require('../libs/api-v2/i18n'); +var buildManifest = require('../libs/api-v2/buildManifest'); var shared = require('../../../common'); var forceRefresh = require('./forceRefresh'); var tavernQuest = require('../models/group').tavernQuest; 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/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 a1500ac39a..f167884245 100644 --- a/website/src/routes/api-v2/swagger.js +++ b/website/src/routes/api-v2/swagger.js @@ -18,7 +18,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 2562c9bb87..1142f27539 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/utils'; import express from 'express'; import http from 'http'; // import path from 'path'; @@ -13,11 +12,11 @@ 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(); // 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'); @@ -42,7 +41,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'; @@ -84,6 +83,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); @@ -95,7 +95,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"); @@ -153,7 +153,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);