diff --git a/package.json b/package.json index bc501098c9..fb07f0753a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "nodetime": "*", "resolve": "~0.2.3", "request": "~2.14.0", - "querystring": "~0.1.0" + "querystring": "~0.1.0", + "nock": "~0.15.2", + "superagent": "~0.12.4" }, "private": true, "subdomain": "habitrpg", diff --git a/server.js b/server.js index 5709a5db48..50493a836c 100644 --- a/server.js +++ b/server.js @@ -67,7 +67,9 @@ process.on('uncaughtException', function (error) { }); require('coffee-script') // remove intermediate compilation requirement -require('./src/server').listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); +module.exports = server = require('./src/server') + +server.listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); // Note: removed "up" module, which is default for development (but interferes with and production + PaaS) // Restore to 5310bb0 if I want it back (see https://github.com/codeparty/derby/issues/165#issuecomment-10405693) diff --git a/src/app/character.coffee b/src/app/character.coffee index 901e3cb518..16d1c40fc5 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -137,7 +137,7 @@ module.exports.updateUser = (model) -> batch = new BatchUpdate(model) user = batch.user obj = batch.obj() - tasks = obj.tasks + tasks = obj?.tasks # Remove corrupted tasks _.each tasks, (task, key) -> @@ -172,49 +172,49 @@ module.exports.BatchUpdate = BatchUpdate = (model) -> updates = {} { - user: user + user: user - obj: -> - obj ?= model.get 'users.'+user.get('id') - return obj + obj: -> + obj ?= model.get 'users.'+user.get('id') + return obj - startTransaction: -> - # start a batch transaction - nothing between now and @commit() will be set immediately - transactionInProgress = true - model._dontPersist = true - @obj() + startTransaction: -> + # start a batch transaction - nothing between now and @commit() will be set immediately + transactionInProgress = true + model._dontPersist = true + @obj() - ### - Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued - but not actually set to the model. It also modifies userObj in case you need to access properties manually later. - If transaction not in progress, it just runs standard model.set() - ### - set: (path, val) -> - updates[path] = val if transactionInProgress - user.set(path, val) + ### + Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued + but not actually set to the model. It also modifies userObj in case you need to access properties manually later. + If transaction not in progress, it just runs standard model.set() + ### + set: (path, val) -> + updates[path] = val if transactionInProgress + user.set(path, val) - ### - Hack to get around dom bindings being lost if parent objects are replaced whole-sale - eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok - ### - setStats: (stats) -> - stats ?= obj.stats - that = @ - _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] + ### + Hack to get around dom bindings being lost if parent objects are replaced whole-sale + eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok + ### + setStats: (stats) -> + stats ?= obj.stats + that = @ + _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] -# queue: (path, val) -> -# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 -# arr = path.split('.') -# arr.reduce (curr, next, index) -> -# if (arr.length - 1) == index -# curr[next] = val -# curr[next] -# , obj + # queue: (path, val) -> + # # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 + # arr = path.split('.') + # arr.reduce (curr, next, index) -> + # if (arr.length - 1) == index + # curr[next] = val + # curr[next] + # , obj - commit: -> - model._dontPersist = false - # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js - user.set "update__", updates - transactionInProgress = false - updates = {} + commit: -> + model._dontPersist = false + # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js + user.set "update__", updates + transactionInProgress = false + updates = {} } diff --git a/src/server/api.coffee b/src/server/api.coffee index 8d2d8e4875..37da942522 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -21,17 +21,19 @@ router.get '/status', (req, res) -> status: 'up' router.get '/user', (req, res) -> + console.log 'hi' { uid, token } = req.query return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) - model.fetch query, (err, user) -> + query.fetch (err, user) -> + self = user.get() return res.json 500, err: err if err - return res.json 500, NO_USER_FOUND if !user || _.isEmpty(user) + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - res.json user + res.json self router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index 44ae3d392c..8e497764ef 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -13,6 +13,31 @@ 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.get '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> + {uid, taskId, direction} = req.params + {apiToken} = req.query + {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() + req._isServer = true + model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> + return res.send(500, err) if err + user = result.at(0) + 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) + + res.send(user) + router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> {uid, taskId, direction} = req.params {apiToken, title, service, icon} = req.body diff --git a/src/server/index.coffee b/src/server/index.coffee index 54e2be229f..ab304c3c72 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -17,9 +17,9 @@ middleware = require './middleware' racer.io.set('transports', ['xhr-polling']) racer.ioClient.set('reconnection limit', 300000) # max reconect timeout to 5 minutes racer.set('bundleTimeout', 40000) -#unless process.env.NODE_ENV == 'production' -# racer.use(racer.logPlugin) -# derby.use(derby.logPlugin) +unless process.env.NODE_ENV == 'production' + racer.use(racer.logPlugin) + derby.use(derby.logPlugin) ## SERVER CONFIGURATION ## @@ -28,7 +28,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 diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index 44f53b90d0..d74eb1953e 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,7 +1,6 @@ module.exports.splash = (req, res, next) -> # This was an API call, not a page load - return next() if req.is('json') - + return next() if /^\/api/.test req.path if !req.session.userId? and !req.query?.play? res.redirect('/splash.html') else diff --git a/src/server/store.coffee b/src/server/store.coffee index 1edada5125..0a10b1b7e8 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -54,12 +54,12 @@ userAccess = (store) -> ### REST = (store) -> store.query.expose "users", "withIdAndToken", (uid, token) -> - @where('id').equals(uid) + @byId(uid) .where('apiToken').equals(token) .one - store.queryAccess "users", "withIdAndToken", (id, token, accept, err) -> - return accept(true) if id && token + store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) -> + return accept(true) if uid && token accept(false) # only user has id & token @@ -90,4 +90,4 @@ partySystem = (store) -> store.writeAccess "*", "parties.*", -> accept = arguments[arguments.length-2] - accept(true) \ No newline at end of file + accept(true) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index ec3d5ca243..7da76094b5 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -1,11 +1,16 @@ assert = require 'assert' {BrowserModel: Model} = require 'racer/test/util/model' derby = require 'derby' +racer = require 'racer' _ = require 'underscore' moment = require 'moment' -request = require 'request' +request = require 'superagent' qs = require 'querystring' +app = require '../server' + +store = app.habitStore + # Custom modules scoring = require '../src/app/scoring' character = require '../src/app/character' @@ -75,27 +80,55 @@ modificationsLookup = (direction, options = {}) -> describe 'API', -> model = null + user = null + params = null describe 'Without token or user id', -> it '/api/v1/user', (done) -> - request.get { uri: "#{baseURL}/user" }, (err, res, body) -> - console.log "#{baseURL}/user", body - assert.ok !err - assert.equal res.statusCode, 500 - assert.ok body.err - done() + request(app) + .get("/api/v1/user") + .set('Accept', 'application/json') + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + assert.equal res.statusCode, 500 + assert.ok JSON.parse(res.text).err + done() describe 'With token and user id', -> - before -> - model = new Model - model.set '_user', character.newUserObject() - scoring.setModel model + before (done) -> + model = store.createModel() + store.flush() + + uid = model.id() + user = character.newUserObject() + user.apiToken = derby.uuid() + model.set "users.#{uid}", user + user = model.get("users.#{uid}") + + params = + uid: user.id + token: user.apiToken + + done() + + after (done) -> + store.flush done it '/api/v1/user', (done) -> - user = model.get '_user' - - request.get { uri: "#{baseURL}/user?#{qs.stringify(UID_AND_TOKEN)}" }, (err, res, body) -> - assert.ok !err - assert.equal res.statusCode, 200 - done() + console.log params + request(app) + .get("/api/v1/user") + .set('Accept', 'application/json') + .query(params) + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + console.log 'test', res.body + assert.ok !res.body.err + assert.equal res.statusCode, 200 + assert.ok res.body + done() diff --git a/test/request/local.coffee b/test/request/local.coffee new file mode 100644 index 0000000000..1431d12cd5 --- /dev/null +++ b/test/request/local.coffee @@ -0,0 +1,25 @@ +Request = require './request' +qs = require 'querystring' + +makeUrl = (path, params) -> + if typeof params is "object" + params = qs.stringify(params) + else + params = '' + return "http://localhost:3000/#{path}?#{params}" + +# url, params (optional), callback +get = (obj, callback) -> + obj.params ||= {} + obj.path ||= '' + + requestUrl = makeUrl obj.path, obj.params + + request = new Request requestUrl + + request.done (res) -> + callback null, JSON.parse(res) + + request.fire() + +module.exports = { get } diff --git a/test/request/request.coffee b/test/request/request.coffee new file mode 100644 index 0000000000..5eadd9178b --- /dev/null +++ b/test/request/request.coffee @@ -0,0 +1,74 @@ +http = require 'http' + +# ### Request +# +# The Request class is just a wrapper +# around the `http.request` function, +# providing a cleaner and more reusable +# interface. +# +# `constructor(options)` +# +# `options` can be anything `http.request` takes. +# +# Callbacks can be queued up by calling `request.done` +# and passing it a callback function to be invoked when the +# http request has completed +# +# Example: +# +# callback = (json) -> +# doSomethingWithJSON( json ) +# +# request = new Request('http://api.phish.net/api.js?') +# request.done (response) -> +# callback JSON.parse(response)[0] +# +# request.fire(data) # data refers to the post data +# +class Request + constructor: (@options) -> + @callbacks = {} + + fire: (data) -> + request = http.request(@options, @_handler) + request.on 'error', (err) => + for callback in @callbacks['fail'] + callback(err) + + # Send data with request + if data + request.write data + + request.end() + + this + + done: (callback) -> + push.call(this, 'done', callback) + + this + + fail: (callback) -> + push.call(this, 'fail', callback) + + this + + ########### + # PRIVATE # + ########### + + _handler: (response) => + data = '' + response.on 'data', (chunk) -> + data += chunk + response.on 'end', => + for callback in @callbacks['done'] + callback(data) + + + push = (type, callback) -> + @callbacks[type] ?= [] + @callbacks[type].push callback + +module.exports = Request