mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-08-05 03:52:14 +00:00
Merge pull request #522 from switz/api
@lefnire => Initial API support including specs
This commit is contained in:
commit
7e78e8cd12
11 changed files with 490 additions and 109 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,4 +4,4 @@ node_modules
|
||||||
#lib/
|
#lib/
|
||||||
*.swp
|
*.swp
|
||||||
.idea*
|
.idea*
|
||||||
config.json
|
config.json
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,10 @@
|
||||||
"mongoskin": "*",
|
"mongoskin": "*",
|
||||||
"nconf": "*",
|
"nconf": "*",
|
||||||
"icalendar": "git://github.com/lefnire/node-icalendar#master",
|
"icalendar": "git://github.com/lefnire/node-icalendar#master",
|
||||||
|
"superagent": "~0.12.4",
|
||||||
"resolve": "~0.2.3",
|
"resolve": "~0.2.3",
|
||||||
"browserify": "1.17.3",
|
"browserify": "1.17.3",
|
||||||
|
"expect.js": "~0.2.0",
|
||||||
"webkit-devtools-agent": "*"
|
"webkit-devtools-agent": "*"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -38,6 +40,7 @@
|
||||||
"npm": "1.1.x"
|
"npm": "1.1.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "server.js"
|
"start": "server.js",
|
||||||
|
"test": "mocha test/api.mocha.coffee"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,56 +3,116 @@ router = new express.Router()
|
||||||
|
|
||||||
scoring = require '../app/scoring'
|
scoring = require '../app/scoring'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
icalendar = require('icalendar')
|
validator = require 'derby-auth/node_modules/validator'
|
||||||
|
check = validator.check
|
||||||
|
sanitize = validator.sanitize
|
||||||
|
icalendar = require 'icalendar'
|
||||||
|
|
||||||
# ---------- /v1 API ------------
|
NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request"
|
||||||
# Every url added beneath router is prefaced by /v1
|
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:
|
v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with:
|
||||||
curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
|
$ cd node_modules/racer && npm install && cd ../..
|
||||||
|
$ mocha test/api.mocha.coffee
|
||||||
###
|
###
|
||||||
|
|
||||||
router.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
|
router.get '/status', (req, res) ->
|
||||||
{uid, taskId, direction} = req.params
|
res.json status: 'up'
|
||||||
{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
|
auth = (req, res, next) ->
|
||||||
return res.send(500, 'request body "apiToken" required') unless apiToken
|
uid = req.headers['x-api-user']
|
||||||
return res.send(500, ':uid required') unless uid
|
token = req.headers['x-api-key']
|
||||||
return res.send(500, ':taskId required') unless taskId
|
return res.json 401, NO_TOKEN_OR_UID unless uid || token
|
||||||
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
|
|
||||||
|
|
||||||
req._isServer = true
|
|
||||||
model = req.getModel()
|
model = req.getModel()
|
||||||
model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) ->
|
query = model.query('users').withIdAndToken(uid, token)
|
||||||
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)
|
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
|
router.get '/user', auth, (req, res) ->
|
||||||
# TODO add service & icon to task
|
user = req.userObj
|
||||||
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 = scoring.Scoring(model)
|
delete user.apiToken
|
||||||
delta = score.score(taskId, direction)
|
|
||||||
result = model.get ('_user.stats')
|
res.json user
|
||||||
result.delta = delta
|
|
||||||
res.send(result)
|
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) ->
|
router.get '/users/:uid/calendar.ics', (req, res) ->
|
||||||
#return next() #disable for now
|
#return next() #disable for now
|
||||||
|
|
@ -62,11 +122,11 @@ router.get '/users/:uid/calendar.ics', (req, res) ->
|
||||||
model = req.getModel()
|
model = req.getModel()
|
||||||
query = model.query('users').withIdAndToken(uid, apiToken)
|
query = model.query('users').withIdAndToken(uid, apiToken)
|
||||||
query.fetch (err, result) ->
|
query.fetch (err, result) ->
|
||||||
return res.send(500, err) if err
|
return res.send(400, err) if err
|
||||||
tasks = result.get('tasks')
|
tasks = result.get('tasks')
|
||||||
# tasks = result[0].tasks
|
# tasks = result[0].tasks
|
||||||
tasksWithDates = _.filter tasks, (task) -> !!task.date
|
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 = new icalendar.iCalendar()
|
||||||
ical.addProperty('NAME', 'HabitRPG')
|
ical.addProperty('NAME', 'HabitRPG')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
express = require 'express'
|
express = require 'express'
|
||||||
router = new express.Router()
|
router = new express.Router()
|
||||||
|
|
||||||
|
scoring = require '../app/scoring'
|
||||||
|
_ = require 'underscore'
|
||||||
|
icalendar = require('icalendar')
|
||||||
|
|
||||||
# ---------- Deprecated Paths ------------
|
# ---------- Deprecated Paths ------------
|
||||||
|
|
||||||
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
|
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.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 '/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
|
module.exports = router
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ server = http.createServer expressApp
|
||||||
module.exports = server
|
module.exports = server
|
||||||
|
|
||||||
derby.use require('racer-db-mongo')
|
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}
|
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
|
||||||
listen: server
|
listen: server
|
||||||
|
|
||||||
|
|
@ -74,6 +74,9 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
|
||||||
)
|
)
|
||||||
# Adds req.getModel method
|
# Adds req.getModel method
|
||||||
.use(store.modelMiddleware())
|
.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
|
# Show splash page for newcomers
|
||||||
.use(middleware.splash)
|
.use(middleware.splash)
|
||||||
.use(priv.middleware)
|
.use(priv.middleware)
|
||||||
|
|
@ -81,9 +84,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
|
||||||
.use(auth.middleware(strategies, options))
|
.use(auth.middleware(strategies, options))
|
||||||
# Creates an express middleware from the app's routes
|
# Creates an express middleware from the app's routes
|
||||||
.use(app.router())
|
.use(app.router())
|
||||||
.use('/v1', require('./api').middleware)
|
|
||||||
.use(require('./static').middleware)
|
.use(require('./static').middleware)
|
||||||
.use(require('./deprecated').middleware)
|
|
||||||
.use(expressApp.router)
|
.use(expressApp.router)
|
||||||
.use(serverError(root))
|
.use(serverError(root))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
module.exports.splash = (req, res, next) ->
|
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')
|
unless req.query?.play? or req.getModel().get('_userId')
|
||||||
res.redirect('/splash.html')
|
res.redirect('/splash.html')
|
||||||
else
|
else
|
||||||
next()
|
next()
|
||||||
|
|
||||||
module.exports.view = (req, res, next) ->
|
module.exports.view = (req, res, next) ->
|
||||||
model = req.getModel()
|
model = req.getModel()
|
||||||
_view = model.get('_view') || {}
|
_view = model.get('_view') || {}
|
||||||
## Set _mobileDevice to true or false so view can exclude portions from mobile device
|
## 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.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent')
|
||||||
_view.nodeEnv = process.env.NODE_ENV
|
_view.nodeEnv = process.env.NODE_ENV
|
||||||
model.set '_view', _view
|
model.set '_view', _view
|
||||||
next()
|
next()
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ userAccess = (store) ->
|
||||||
store.writeAccess "*", "users.*", -> # captures, value, accept, err ->
|
store.writeAccess "*", "users.*", -> # captures, value, accept, err ->
|
||||||
accept = arguments[arguments.length-2]
|
accept = arguments[arguments.length-2]
|
||||||
err = arguments[arguments.length - 1]
|
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(@)
|
return accept(false) if derbyAuth.bustedSession(@)
|
||||||
|
|
||||||
captures = arguments[0].split('.')
|
captures = arguments[0].split('.')
|
||||||
|
|
@ -40,8 +43,7 @@ userAccess = (store) ->
|
||||||
return accept(true)
|
return accept(true)
|
||||||
|
|
||||||
# Same session (user.id = this.session.userId)
|
# Same session (user.id = this.session.userId)
|
||||||
if (uid is @session.userId) or derbyAuth.isServer(@)
|
return accept(true) if uid is @session.userId
|
||||||
return accept(true)
|
|
||||||
|
|
||||||
accept(false)
|
accept(false)
|
||||||
|
|
||||||
|
|
|
||||||
245
test/api.mocha.coffee
Normal file
245
test/api.mocha.coffee
Normal file
|
|
@ -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()
|
||||||
|
|
@ -12,8 +12,8 @@ casper.start "#{url}/?play=1", ->
|
||||||
@fill 'form#derby-auth-register',
|
@fill 'form#derby-auth-register',
|
||||||
username: user1.id
|
username: user1.id
|
||||||
email: "{user1.id}@gmail.com"
|
email: "{user1.id}@gmail.com"
|
||||||
'email-confirmation': "{user1.id}@gmail.com"
|
|
||||||
password: 'habitrpg123'
|
password: 'habitrpg123'
|
||||||
|
'password-confirmation': "habitrpg123"
|
||||||
, true
|
, true
|
||||||
casper.thenOpen "#{url}/logout"
|
casper.thenOpen "#{url}/logout"
|
||||||
casper.thenOpen "#{url}/?play=1", ->
|
casper.thenOpen "#{url}/?play=1", ->
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
--colors
|
--colors
|
||||||
--reporter spec
|
--reporter spec
|
||||||
--timeout 1200
|
--timeout 2800
|
||||||
|
--ignore-leaks
|
||||||
--growl
|
--growl
|
||||||
--debug
|
--debug
|
||||||
--compilers coffee:coffee-script
|
--compilers coffee:coffee-script
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ moment = require 'moment'
|
||||||
# Custom modules
|
# Custom modules
|
||||||
scoring = require '../src/app/scoring'
|
scoring = require '../src/app/scoring'
|
||||||
schema = require '../src/app/schema'
|
schema = require '../src/app/schema'
|
||||||
|
|
||||||
###### Helpers & Variables ######
|
###### Helpers & Variables ######
|
||||||
|
|
||||||
model = null
|
model = null
|
||||||
|
|
@ -19,11 +19,11 @@ taskPath = null
|
||||||
# Otherwise, using model.get(path) will give the same object before as after
|
# Otherwise, using model.get(path) will give the same object before as after
|
||||||
pathSnapshots = (paths) ->
|
pathSnapshots = (paths) ->
|
||||||
if _.isString(paths)
|
if _.isString(paths)
|
||||||
return clone(model.get(paths))
|
return clone(model.get(paths))
|
||||||
_.map paths, (path) -> clone(model.get(path))
|
_.map paths, (path) -> clone(model.get(path))
|
||||||
statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task
|
statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task
|
||||||
|
|
||||||
cleanUserObj = ->
|
cleanUserObj = ->
|
||||||
userObj = schema.newUserObject()
|
userObj = schema.newUserObject()
|
||||||
userObj.tasks = {}
|
userObj.tasks = {}
|
||||||
userObj.habitIds = []
|
userObj.habitIds = []
|
||||||
|
|
@ -42,14 +42,14 @@ freshTask = (taskObj) ->
|
||||||
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
|
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
|
||||||
[taskObj.id, taskObj.value] = [uuid, 0]
|
[taskObj.id, taskObj.value] = [uuid, 0]
|
||||||
model.at("_#{type}List").push taskObj
|
model.at("_#{type}List").push taskObj
|
||||||
|
|
||||||
###
|
###
|
||||||
Helper function to determine if stats updates are numerically correct based on scoring
|
Helper function to determine if stats updates are numerically correct based on scoring
|
||||||
@direction: 'up' or 'down'
|
@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 = {}) ->
|
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
|
{times, lvl, armor, weapon} = merged
|
||||||
userObj = cleanUserObj()
|
userObj = cleanUserObj()
|
||||||
value = 0
|
value = 0
|
||||||
|
|
@ -64,12 +64,12 @@ modificationsLookup = (direction, options = {}) ->
|
||||||
loss = scoring.hpModifier(delta, options)
|
loss = scoring.hpModifier(delta, options)
|
||||||
userObj.stats.hp += loss
|
userObj.stats.hp += loss
|
||||||
return {user:userObj, value:value}
|
return {user:userObj, value:value}
|
||||||
|
|
||||||
###### Specs ######
|
###### Specs ######
|
||||||
|
|
||||||
describe 'User', ->
|
describe 'User', ->
|
||||||
model = null
|
model = null
|
||||||
|
|
||||||
before ->
|
before ->
|
||||||
model = new Model
|
model = new Model
|
||||||
model.set '_user', schema.newUserObject()
|
model.set '_user', schema.newUserObject()
|
||||||
|
|
@ -85,23 +85,23 @@ describe 'User', ->
|
||||||
expect(_.size(user.dailyIds)).to.eql 3
|
expect(_.size(user.dailyIds)).to.eql 3
|
||||||
expect(_.size(user.todoIds)).to.eql 1
|
expect(_.size(user.todoIds)).to.eql 1
|
||||||
expect(_.size(user.rewardIds)).to.eql 2
|
expect(_.size(user.rewardIds)).to.eql 2
|
||||||
|
|
||||||
##### Habits #####
|
##### Habits #####
|
||||||
describe 'Tasks', ->
|
describe 'Tasks', ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
resetUser()
|
resetUser()
|
||||||
|
|
||||||
describe 'Habits', ->
|
describe 'Habits', ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
|
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
|
||||||
|
|
||||||
it 'created the habit', ->
|
it 'created the habit', ->
|
||||||
task = model.get(taskPath)
|
task = model.get(taskPath)
|
||||||
expect(task.text).to.eql 'Habit'
|
expect(task.text).to.eql 'Habit'
|
||||||
expect(task.value).to.eql 0
|
expect(task.value).to.eql 0
|
||||||
|
|
||||||
it 'test a few scoring numbers (this will change if constants / formulae change)', ->
|
it 'test a few scoring numbers (this will change if constants / formulae change)', ->
|
||||||
{user} = modificationsLookup('down')
|
{user} = modificationsLookup('down')
|
||||||
expect(user.stats.hp).to.eql 49
|
expect(user.stats.hp).to.eql 49
|
||||||
|
|
@ -113,10 +113,10 @@ describe 'User', ->
|
||||||
expect(user.stats.money).to.eql 1
|
expect(user.stats.money).to.eql 1
|
||||||
{user} = modificationsLookup('up', {times:5})
|
{user} = modificationsLookup('up', {times:5})
|
||||||
expect(user.stats.exp).to.be.within(4,5)
|
expect(user.stats.exp).to.be.within(4,5)
|
||||||
|
|
||||||
it 'made proper modifications when down-scored', ->
|
it 'made proper modifications when down-scored', ->
|
||||||
## Trial 1
|
## Trial 1
|
||||||
|
|
||||||
shouldBe = modificationsLookup('down')
|
shouldBe = modificationsLookup('down')
|
||||||
scoring.score(uuid,'down')
|
scoring.score(uuid,'down')
|
||||||
[stats, task] = statsTask()
|
[stats, task] = statsTask()
|
||||||
|
|
@ -130,14 +130,14 @@ describe 'User', ->
|
||||||
[stats, task] = statsTask()
|
[stats, task] = statsTask()
|
||||||
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
|
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
|
||||||
expect(task.value).to.eql shouldBe.value
|
expect(task.value).to.eql shouldBe.value
|
||||||
|
|
||||||
it 'made proper modifications when up-scored', ->
|
it 'made proper modifications when up-scored', ->
|
||||||
# Up-score the habit
|
# Up-score the habit
|
||||||
[statsBefore, taskBefore] = statsTask()
|
[statsBefore, taskBefore] = statsTask()
|
||||||
scoring.score(uuid, 'up')
|
scoring.score(uuid, 'up')
|
||||||
[statsAfter, taskAfter] = statsTask()
|
[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.exp).to.be.greaterThan statsBefore.exp
|
||||||
expect(statsAfter.money).to.be.greaterThan statsBefore.money
|
expect(statsAfter.money).to.be.greaterThan statsBefore.money
|
||||||
# HP should not change
|
# HP should not change
|
||||||
|
|
@ -145,9 +145,9 @@ describe 'User', ->
|
||||||
# Task should have lost value
|
# Task should have lost value
|
||||||
expect(taskBefore.value).to.eql 0
|
expect(taskBefore.value).to.eql 0
|
||||||
expect(taskAfter.value).to.be.greaterThan taskBefore.value
|
expect(taskAfter.value).to.be.greaterThan taskBefore.value
|
||||||
|
|
||||||
## Trial 2
|
## Trial 2
|
||||||
taskBefore = pathSnapshots(taskPath)
|
taskBefore = pathSnapshots(taskPath)
|
||||||
scoring.score(uuid, 'up')
|
scoring.score(uuid, 'up')
|
||||||
taskAfter = pathSnapshots(taskPath)
|
taskAfter = pathSnapshots(taskPath)
|
||||||
# Should have lost in value
|
# Should have lost in value
|
||||||
|
|
@ -155,54 +155,54 @@ describe 'User', ->
|
||||||
# And lost more than trial 1
|
# And lost more than trial 1
|
||||||
diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value)
|
diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value)
|
||||||
expect(diff).to.be.lessThan 1
|
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'
|
it 'makes proper modifications each time when clicking + / - in rapid succession'
|
||||||
# saw an issue here once, so test that it wasn't a fluke
|
# saw an issue here once, so test that it wasn't a fluke
|
||||||
|
|
||||||
it 'should not modify certain attributes given certain conditions'
|
it 'should not modify certain attributes given certain conditions'
|
||||||
# non up+down habits
|
# non up+down habits
|
||||||
# what else?
|
# what else?
|
||||||
|
|
||||||
it 'should show "undo" notification if user unchecks completed daily'
|
it 'should show "undo" notification if user unchecks completed daily'
|
||||||
|
|
||||||
|
|
||||||
describe 'Lvl & Items', ->
|
describe 'Lvl & Items', ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
|
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
|
||||||
|
|
||||||
it 'modified damage based on lvl & armor'
|
it 'modified damage based on lvl & armor'
|
||||||
it 'modified exp/gp based on lvl & weapon'
|
it 'modified exp/gp based on lvl & weapon'
|
||||||
it 'always decreases hp with damage, regardless of stats/items'
|
it 'always decreases hp with damage, regardless of stats/items'
|
||||||
it 'always increases exp/gp with gain, regardless of stats/items'
|
it 'always increases exp/gp with gain, regardless of stats/items'
|
||||||
|
|
||||||
describe 'Dailies', ->
|
describe 'Dailies', ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
freshTask {type: 'daily', text: 'Daily', completed: false}
|
freshTask {type: 'daily', text: 'Daily', completed: false}
|
||||||
|
|
||||||
it 'created the daily', ->
|
it 'created the daily', ->
|
||||||
task = model.get(taskPath)
|
task = model.get(taskPath)
|
||||||
expect(task.text).to.eql 'Daily'
|
expect(task.text).to.eql 'Daily'
|
||||||
expect(task.value).to.eql 0
|
expect(task.value).to.eql 0
|
||||||
|
|
||||||
it 'does proper calculations when daily is complete'
|
it 'does proper calculations when daily is complete'
|
||||||
it 'calculates dailys properly when they have repeat dates'
|
it 'calculates dailys properly when they have repeat dates'
|
||||||
|
|
||||||
runCron = (times, pass=1) ->
|
runCron = (times, pass=1) ->
|
||||||
# Set lastCron to days ago
|
# Set lastCron to days ago
|
||||||
today = new moment()
|
today = new moment()
|
||||||
ago = new moment().subtract('days',times)
|
ago = new moment().subtract('days',times)
|
||||||
model.set '_user.lastCron', ago.toDate()
|
model.set '_user.lastCron', ago.toDate()
|
||||||
# Run run
|
# Run run
|
||||||
scoring.cron()
|
scoring.cron()
|
||||||
[stats, task] = statsTask()
|
[stats, task] = statsTask()
|
||||||
|
|
||||||
# Should have updated cron to today
|
# Should have updated cron to today
|
||||||
lastCron = moment(model.get('_user.lastCron'))
|
lastCron = moment(model.get('_user.lastCron'))
|
||||||
expect(today.diff(lastCron, 'days')).to.eql 0
|
expect(today.diff(lastCron, 'days')).to.eql 0
|
||||||
|
|
||||||
shouldBe = modificationsLookup('down', {times:times*pass})
|
shouldBe = modificationsLookup('down', {times:times*pass})
|
||||||
# Should have updated points properly
|
# Should have updated points properly
|
||||||
expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp)
|
expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp)
|
||||||
|
|
@ -215,10 +215,10 @@ describe 'User', ->
|
||||||
runCron(5)
|
runCron(5)
|
||||||
runCron(5, 2)
|
runCron(5, 2)
|
||||||
|
|
||||||
|
|
||||||
#TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page
|
#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
|
#TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday
|
||||||
|
|
||||||
describe 'Todos', ->
|
describe 'Todos', ->
|
||||||
describe 'Cron', ->
|
describe 'Cron', ->
|
||||||
it 'calls cron asyncronously'
|
it 'calls cron asyncronously'
|
||||||
|
|
@ -230,11 +230,11 @@ describe 'User', ->
|
||||||
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
|
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
|
||||||
it 'should defer saving user modifications until, save as aggregate values'
|
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)
|
# 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', ->
|
describe 'Rewards', ->
|
||||||
|
|
||||||
#### Require.js stuff, might be necessary to place in casper.coffee
|
#### 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"
|
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
|
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards
|
||||||
Loading…
Reference in a new issue