diff --git a/.gitignore b/.gitignore index f54c5f2a27..029b274b27 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ node_modules #lib/ *.swp .idea* -config.json \ No newline at end of file +config.json diff --git a/package.json b/package.json index 258bab8e95..a80458cca0 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "mongoskin": "*", "nconf": "*", "icalendar": "git://github.com/lefnire/node-icalendar#master", + "superagent": "~0.12.4", "resolve": "~0.2.3", "browserify": "1.17.3", + "expect.js": "~0.2.0", "webkit-devtools-agent": "*" }, "private": true, @@ -38,6 +40,7 @@ "npm": "1.1.x" }, "scripts": { - "start": "server.js" + "start": "server.js", + "test": "mocha test/api.mocha.coffee" } } diff --git a/src/server/api.coffee b/src/server/api.coffee index ef18e8d233..287b354f5d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -3,56 +3,116 @@ router = new express.Router() scoring = require '../app/scoring' _ = require 'underscore' -icalendar = require('icalendar') +validator = require 'derby-auth/node_modules/validator' +check = validator.check +sanitize = validator.sanitize +icalendar = require 'icalendar' -# ---------- /v1 API ------------ -# Every url added beneath router is prefaced by /v1 +NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" +NO_USER_FOUND = err: "No user found." + +# ---------- /api/v1 API ------------ +# Every url added beneath router is prefaced by /api/v1 ### - v1 API. Requires user-id and apiToken, task-id, direction. Test with: - curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up + v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/api.mocha.coffee ### -router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> - {uid, taskId, direction} = req.params - {apiToken, title, service, icon} = req.body - console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' +router.get '/status', (req, res) -> + res.json status: 'up' - # Send error responses for improper API call - return res.send(500, 'request body "apiToken" required') unless apiToken - return res.send(500, ':uid required') unless uid - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] +auth = (req, res, next) -> + uid = req.headers['x-api-user'] + token = req.headers['x-api-key'] + return res.json 401, NO_TOKEN_OR_UID unless uid || token - req._isServer = true model = req.getModel() - model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> - return res.send(500, err) if err - user = result - userObj = user.get() - if _.isEmpty(userObj) - return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + query = model.query('users').withIdAndToken(uid, token) - model.ref('_user', user) + query.fetch (err, user) -> + return res.json err: err if err + req.user = user + req.userObj = user.get() + return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) + req._isServer = true + next() - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks.#{taskId}") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - model.at('_habitList').push - id: taskId - type: 'habit' - text: (title || taskId) - value: 0 - up: true - down: true - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." +router.get '/user', auth, (req, res) -> + user = req.userObj - score = scoring.Scoring(model) - delta = score.score(taskId, direction) - result = model.get ('_user.stats') - result.delta = delta - res.send(result) + delete user.apiToken + + res.json user + +router.get '/user/task/:id', auth, (req, res) -> + task = req.userObj.tasks[req.params.id] + return res.json 400, err: "No task found." if !task || _.isEmpty(task) + + res.json 200, task + +validateTask = (req, res, next) -> + task = {} + newTask = { type, text, notes, value, up, down, completed } = req.body + + # If we're updating, get the task from the user + if req.method is 'PUT' + task = req.userObj?.tasks[req.params.id] + return res.json 400, err: "No task found." if !task || _.isEmpty(task) + # Strip for now + type = undefined + delete newTask.type + else if req.method is 'POST' + unless /^(habit|todo|daily|reward)$/.test type + return res.json 400, err: 'type must be habit, todo, daily, or reward' + + text = sanitize(text).xss() + notes = sanitize(notes).xss() + value = sanitize(value).toInt() + + switch type + when 'habit' + newTask.up = true unless typeof up is 'boolean' + newTask.down = true unless typeof down is 'boolean' + when 'daily', 'todo' + newTask.completed = false unless typeof completed is 'boolean' + + _.extend task, newTask + req.task = task + next() + +router.put '/user/task/:id', auth, validateTask, (req, res) -> + req.user.set "tasks.#{req.task.id}", req.task + + res.json 200, req.task + +router.post '/user/task', auth, validateTask, (req, res) -> + task = req.task + type = task.type + + model = req.getModel() + model.ref '_user', req.user + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.at("_#{type}List").push task + + res.json 201, task + +router.get '/user/tasks', auth, (req, res) -> + user = req.userObj + return res.json 400, NO_USER_FOUND if !user || _.isEmpty(user) + + model = req.getModel() + model.ref '_user', req.user + tasks = [] + types = ['habit','todo','daily','reward'] + if /^(habit|todo|daily|reward)$/.test req.query.type + types = [req.query.type] + for type in types + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + tasks = tasks.concat model.get("_#{type}List") + + res.json 200, tasks router.get '/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now @@ -62,11 +122,11 @@ router.get '/users/:uid/calendar.ics', (req, res) -> model = req.getModel() query = model.query('users').withIdAndToken(uid, apiToken) query.fetch (err, result) -> - return res.send(500, err) if err + return res.send(400, err) if err tasks = result.get('tasks') # tasks = result[0].tasks tasksWithDates = _.filter tasks, (task) -> !!task.date - return res.send(500, "No events found") if _.isEmpty(tasksWithDates) + return res.send(400, "No events found") if _.isEmpty(tasksWithDates) ical = new icalendar.iCalendar() ical.addProperty('NAME', 'HabitRPG') diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index e8a5fb7340..d4e5d3b694 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -1,6 +1,10 @@ express = require 'express' router = new express.Router() +scoring = require '../app/scoring' +_ = require 'underscore' +icalendar = require('icalendar') + # ---------- Deprecated Paths ------------ deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol' @@ -9,4 +13,72 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) +router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> + {uid, taskId, direction} = req.params + {apiToken, title, service, icon} = req.body + console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' + + # Send error responses for improper API call + return res.send(500, 'request body "apiToken" required') unless apiToken + return res.send(500, ':uid required') unless uid + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> + return res.send(500, err) if err + user = result + userObj = user.get() + if _.isEmpty(userObj) + return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + + model.ref('_user', user) + req._isServer = true + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks.#{taskId}") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + model.at('_habitList').push + id: taskId + type: 'habit' + text: (title || taskId) + value: 0 + up: true + down: true + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + score = new scoring.Scoring(model) + delta = score.score(taskId, direction) + result = model.get ('_user.stats') + result.delta = delta + res.send(result) + +router.get '/v1/users/:uid/calendar.ics', (req, res) -> + #return next() #disable for now + {uid} = req.params + {apiToken} = req.query + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, apiToken) + query.fetch (err, result) -> + return res.send(500, err) if err + tasks = result.get('tasks') + # tasks = result[0].tasks + tasksWithDates = _.filter tasks, (task) -> !!task.date + return res.send(500, "No events found") if _.isEmpty(tasksWithDates) + + ical = new icalendar.iCalendar() + ical.addProperty('NAME', 'HabitRPG') + _.each tasksWithDates, (task) -> + event = new icalendar.VEvent(task.id); + event.setSummary(task.text); + d = new Date(task.date) + d.date_only = true + event.setDate d + ical.addComponent event + res.type('text/calendar') + formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') + res.send(200, formattedIcal) + module.exports = router diff --git a/src/server/index.coffee b/src/server/index.coffee index 05beb1f7ec..41b447f677 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -31,7 +31,7 @@ server = http.createServer expressApp module.exports = server derby.use require('racer-db-mongo') -store = derby.createStore +module.exports.habitStore = store = derby.createStore db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true} listen: server @@ -74,6 +74,9 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> ) # Adds req.getModel method .use(store.modelMiddleware()) + # API should be hit before all other routes + .use('/api/v1', require('./api').middleware) + .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) .use(priv.middleware) @@ -81,9 +84,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(auth.middleware(strategies, options)) # Creates an express middleware from the app's routes .use(app.router()) - .use('/v1', require('./api').middleware) .use(require('./static').middleware) - .use(require('./deprecated').middleware) .use(expressApp.router) .use(serverError(root)) diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index 03e388ffee..9ad133788d 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,17 +1,14 @@ module.exports.splash = (req, res, next) -> - # This was an API call, not a page load - return next() if req.is('json') - unless req.query?.play? or req.getModel().get('_userId') res.redirect('/splash.html') else next() module.exports.view = (req, res, next) -> - model = req.getModel() - _view = model.get('_view') || {} - ## Set _mobileDevice to true or false so view can exclude portions from mobile device - _view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent') - _view.nodeEnv = process.env.NODE_ENV - model.set '_view', _view - next() + model = req.getModel() + _view = model.get('_view') || {} + ## Set _mobileDevice to true or false so view can exclude portions from mobile device + _view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent') + _view.nodeEnv = process.env.NODE_ENV + model.set '_view', _view + next() diff --git a/src/server/store.coffee b/src/server/store.coffee index 3ee0f45b24..ac4ab491af 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -28,7 +28,10 @@ userAccess = (store) -> store.writeAccess "*", "users.*", -> # captures, value, accept, err -> accept = arguments[arguments.length-2] err = arguments[arguments.length - 1] -# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) + # return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) + + return accept(true) if derbyAuth.isServer(@) + return accept(false) if derbyAuth.bustedSession(@) captures = arguments[0].split('.') @@ -40,8 +43,7 @@ userAccess = (store) -> return accept(true) # Same session (user.id = this.session.userId) - if (uid is @session.userId) or derbyAuth.isServer(@) - return accept(true) + return accept(true) if uid is @session.userId accept(false) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee new file mode 100644 index 0000000000..6d82d71516 --- /dev/null +++ b/test/api.mocha.coffee @@ -0,0 +1,245 @@ +_ = require 'underscore' +request = require 'superagent' +expect = require 'expect.js' +require 'coffee-script' + +conf = require("nconf") +conf.argv().env().file({file: __dirname + '../config.json'}).defaults + +# Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) +#FIXME can't get nconf file above to load... +process.env.BASE_URL = conf.get("BASE_URL") +process.env.FACEBOOK_KEY = conf.get("FACEBOOK_KEY") +process.env.FACEBOOK_SECRET = conf.get("FACEBOOK_SECRET") +process.env.NODE_DB_URI = 'mongodb://localhost/habirpg' + +## monkey-patch expect.js for better diffs on mocha +## see: https://github.com/LearnBoost/expect.js/pull/34 + +origBe = expect.Assertion::be +expect.Assertion::be = expect.Assertion::equal = (obj) -> + @_expected = obj + origBe.call this, obj + +# Custom modules +character = require '../src/app/character' + +###### Helpers & Variables ###### + +model = null +uuid = null +taskPath = null +baseURL = 'http://localhost:1337/api/v1' + +###### Specs ###### + +describe 'API', -> + server = null + store = null + model = null + user = null + uid = null + + before (done) -> + server = require '../src/server' + server.listen '1337', '0.0.0.0' + server.on 'listening', (data) -> + store = server.habitStore + #store.flush() + model = store.createModel() + model.set '_userId', uid = model.id() + user = character.newUserObject() + user.apiToken = model.id() + model.session = {userId:uid} + model.set "users.#{uid}", user + delete model.session + # Crappy hack to let server start before tests run + setTimeout done, 2000 + + describe 'Without token or user id', -> + + it '/api/v1/status', (done) -> + request.get("#{baseURL}/status") + .set('Accept', 'application/json') + .end (res) -> + expect(res.statusCode).to.be 200 + expect(res.body.status).to.be 'up' + done() + + it '/api/v1/user', (done) -> + request.get("#{baseURL}/user") + .set('Accept', 'application/json') + .end (res) -> + expect(res.statusCode).to.be 401 + expect(res.body.err).to.be 'You must include a token and uid (user id) in your request' + done() + + describe 'With token and user id', -> + params = null + currentUser = null + + before -> + user = model.at("users.#{uid}") + currentUser = user.get() + params = + title: 'Title' + text: 'Text' + type: 'habit' + + beforeEach -> + currentUser = user.get() + + it 'GET /api/v1/user', (done) -> + request.get("#{baseURL}/user") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body.id).not.to.be.empty() + self = _.clone(currentUser) + delete self.apiToken + + expect(res.body).to.eql self + done() + + it 'GET /api/v1/user/task/:id', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[0] + request.get("#{baseURL}/user/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body).to.eql currentUser.tasks[tid] + done() + + it 'POST /api/v1/user/task', (done) -> + request.post("#{baseURL}/user/task") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(params) + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 201 + expect(res.body.id).not.to.be.empty() + # Ensure that user owns the newly created object + expect(user.get().tasks[res.body.id]).to.be.an('object') + done() + + it 'POST /api/v1/user/task (without type)', (done) -> + request.post("#{baseURL}/user/task") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send({}) + .end (res) -> + expect(res.body.err).to.be 'type must be habit, todo, daily, or reward' + expect(res.statusCode).to.be 400 + done() + + it 'POST /api/v1/user/task (only type)', (done) -> + request.post("#{baseURL}/user/task") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(type: 'habit') + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 201 + expect(res.body.id).not.to.be.empty() + # Ensure that user owns the newly created object + expect(user.get().tasks[res.body.id]).to.be.an('object') + done() + + it 'PUT /api/v1/user/task/:id', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[0] + request.put("#{baseURL}/user/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(text: 'bye') + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser.tasks[tid].text = 'bye' + expect(res.body).to.eql currentUser.tasks[tid] + done() + + it 'PUT /api/v1/user/task/:id (shouldnt update type)', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[1] + type = if currentUser.tasks[tid].type is 'habit' then 'daily' else 'habit' + request.put("#{baseURL}/user/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(type: type, text: 'fishman') + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser.tasks[tid].text = 'fishman' + expect(res.body).to.eql currentUser.tasks[tid] + done() + + it 'PUT /api/v1/user/task/:id (update notes)', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[2] + request.put("#{baseURL}/user/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(text: 'hi',notes:'foobar matey') + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser.tasks[tid].text = 'hi' + currentUser.tasks[tid].notes = 'foobar matey' + expect(res.body).to.eql currentUser.tasks[tid] + done() + + it 'GET /api/v1/user/tasks', (done) -> + request.get("#{baseURL}/user/tasks") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + model.ref '_user', user + tasks = [] + for type in ['habit','todo','daily','reward'] + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + tasks = tasks.concat model.get("_#{type}List") + # Ensure that user owns the tasks + expect(res.body.length).to.equal tasks.length + # Ensure that the two sets are equal + expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 + done() + + it 'GET /api/v1/user/tasks (todos)', (done) -> + request.get("#{baseURL}/user/tasks") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .query(type:'todo') + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + model.ref '_user', user + model.refList "_todoList", "_user.tasks", "_user.todoIds" + tasks = model.get("_todoList") + # Ensure that user owns the tasks + expect(res.body.length).to.equal tasks.length + # Ensure that the two sets are equal + expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 + done() diff --git a/test/casper/rest.casper.coffee b/test/casper/rest.casper.coffee index 18aaf3afde..767d93489e 100644 --- a/test/casper/rest.casper.coffee +++ b/test/casper/rest.casper.coffee @@ -12,8 +12,8 @@ casper.start "#{url}/?play=1", -> @fill 'form#derby-auth-register', username: user1.id email: "{user1.id}@gmail.com" - 'email-confirmation': "{user1.id}@gmail.com" password: 'habitrpg123' + 'password-confirmation': "habitrpg123" , true casper.thenOpen "#{url}/logout" casper.thenOpen "#{url}/?play=1", -> diff --git a/test/mocha.opts b/test/mocha.opts index d6ef947989..72889931b5 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,6 +1,7 @@ --colors --reporter spec ---timeout 1200 +--timeout 2800 +--ignore-leaks --growl --debug --compilers coffee:coffee-script diff --git a/test/user.mocha.coffee b/test/user.mocha.coffee index 33b1917014..3f4cc1ec5c 100644 --- a/test/user.mocha.coffee +++ b/test/user.mocha.coffee @@ -8,7 +8,7 @@ moment = require 'moment' # Custom modules scoring = require '../src/app/scoring' schema = require '../src/app/schema' - + ###### Helpers & Variables ###### model = null @@ -19,11 +19,11 @@ taskPath = null # Otherwise, using model.get(path) will give the same object before as after pathSnapshots = (paths) -> if _.isString(paths) - return clone(model.get(paths)) + return clone(model.get(paths)) _.map paths, (path) -> clone(model.get(path)) statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task -cleanUserObj = -> +cleanUserObj = -> userObj = schema.newUserObject() userObj.tasks = {} userObj.habitIds = [] @@ -42,14 +42,14 @@ freshTask = (taskObj) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" [taskObj.id, taskObj.value] = [uuid, 0] model.at("_#{type}List").push taskObj - + ### Helper function to determine if stats updates are numerically correct based on scoring @direction: 'up' or 'down' -@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} +@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} ### modificationsLookup = (direction, options = {}) -> - merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} + merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} {times, lvl, armor, weapon} = merged userObj = cleanUserObj() value = 0 @@ -64,12 +64,12 @@ modificationsLookup = (direction, options = {}) -> loss = scoring.hpModifier(delta, options) userObj.stats.hp += loss return {user:userObj, value:value} - -###### Specs ###### + +###### Specs ###### describe 'User', -> model = null - + before -> model = new Model model.set '_user', schema.newUserObject() @@ -85,23 +85,23 @@ describe 'User', -> expect(_.size(user.dailyIds)).to.eql 3 expect(_.size(user.todoIds)).to.eql 1 expect(_.size(user.rewardIds)).to.eql 2 - - ##### Habits ##### + + ##### Habits ##### describe 'Tasks', -> - - beforeEach -> + + beforeEach -> resetUser() - + describe 'Habits', -> - - beforeEach -> + + beforeEach -> freshTask {type: 'habit', text: 'Habit', up: true, down: true} - + it 'created the habit', -> task = model.get(taskPath) expect(task.text).to.eql 'Habit' expect(task.value).to.eql 0 - + it 'test a few scoring numbers (this will change if constants / formulae change)', -> {user} = modificationsLookup('down') expect(user.stats.hp).to.eql 49 @@ -113,10 +113,10 @@ describe 'User', -> expect(user.stats.money).to.eql 1 {user} = modificationsLookup('up', {times:5}) expect(user.stats.exp).to.be.within(4,5) - + it 'made proper modifications when down-scored', -> ## Trial 1 - + shouldBe = modificationsLookup('down') scoring.score(uuid,'down') [stats, task] = statsTask() @@ -130,14 +130,14 @@ describe 'User', -> [stats, task] = statsTask() expect(stats.hp).to.be.eql shouldBe.user.stats.hp expect(task.value).to.eql shouldBe.value - + it 'made proper modifications when up-scored', -> # Up-score the habit [statsBefore, taskBefore] = statsTask() scoring.score(uuid, 'up') [statsAfter, taskAfter] = statsTask() - - # User should have gained Exp, GP + + # User should have gained Exp, GP expect(statsAfter.exp).to.be.greaterThan statsBefore.exp expect(statsAfter.money).to.be.greaterThan statsBefore.money # HP should not change @@ -145,9 +145,9 @@ describe 'User', -> # Task should have lost value expect(taskBefore.value).to.eql 0 expect(taskAfter.value).to.be.greaterThan taskBefore.value - + ## Trial 2 - taskBefore = pathSnapshots(taskPath) + taskBefore = pathSnapshots(taskPath) scoring.score(uuid, 'up') taskAfter = pathSnapshots(taskPath) # Should have lost in value @@ -155,54 +155,54 @@ describe 'User', -> # And lost more than trial 1 diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value) expect(diff).to.be.lessThan 1 - - it 'makes history entry for habit' + + it 'makes history entry for habit' it 'makes proper modifications each time when clicking + / - in rapid succession' # saw an issue here once, so test that it wasn't a fluke - + it 'should not modify certain attributes given certain conditions' # non up+down habits # what else? - + it 'should show "undo" notification if user unchecks completed daily' - - - describe 'Lvl & Items', -> - - beforeEach -> + + + describe 'Lvl & Items', -> + + beforeEach -> freshTask {type: 'habit', text: 'Habit', up: true, down: true} - + it 'modified damage based on lvl & armor' it 'modified exp/gp based on lvl & weapon' it 'always decreases hp with damage, regardless of stats/items' it 'always increases exp/gp with gain, regardless of stats/items' - + describe 'Dailies', -> - + beforeEach -> freshTask {type: 'daily', text: 'Daily', completed: false} - + it 'created the daily', -> task = model.get(taskPath) expect(task.text).to.eql 'Daily' expect(task.value).to.eql 0 - + it 'does proper calculations when daily is complete' it 'calculates dailys properly when they have repeat dates' - + runCron = (times, pass=1) -> # Set lastCron to days ago today = new moment() ago = new moment().subtract('days',times) model.set '_user.lastCron', ago.toDate() # Run run - scoring.cron() + scoring.cron() [stats, task] = statsTask() - + # Should have updated cron to today lastCron = moment(model.get('_user.lastCron')) expect(today.diff(lastCron, 'days')).to.eql 0 - + shouldBe = modificationsLookup('down', {times:times*pass}) # Should have updated points properly expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp) @@ -215,10 +215,10 @@ describe 'User', -> runCron(5) runCron(5, 2) - + #TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page #TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday - + describe 'Todos', -> describe 'Cron', -> it 'calls cron asyncronously' @@ -230,11 +230,11 @@ describe 'User', -> # stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score() it 'should defer saving user modifications until, save as aggregate values' # pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc) - + describe 'Rewards', -> - + #### Require.js stuff, might be necessary to place in casper.coffee it "doesn't setup dependent functions until their modules are loaded, require.js callback" - # sortable, stripe, etc + # sortable, stripe, etc #TODO refactor as user->habits, user->dailys, user->todos, user->rewards \ No newline at end of file