mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Refactor /db/article to use generators
This commit is contained in:
parent
1d57199707
commit
7fb08f343a
23 changed files with 1450 additions and 135 deletions
|
@ -56,6 +56,8 @@
|
|||
"async": "0.2.x",
|
||||
"aws-sdk": "~2.0.0",
|
||||
"bayesian-battle": "0.0.7",
|
||||
"bluebird": "^3.2.1",
|
||||
"co-express": "^1.2.1",
|
||||
"coffee-script": "1.9.x",
|
||||
"connect": "2.7.x",
|
||||
"express": "~3.0.6",
|
||||
|
|
|
@ -36,4 +36,8 @@ ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']})
|
|||
ArticleSchema.plugin(plugins.TranslationCoveragePlugin)
|
||||
ArticleSchema.plugin(plugins.PatchablePlugin)
|
||||
|
||||
ArticleSchema.postEditableProperties = []
|
||||
ArticleSchema.editableProperties = ['body', 'name', 'i18n', 'i18nCoverage']
|
||||
ArticleSchema.jsonSchema = require '../../app/schemas/models/article'
|
||||
|
||||
module.exports = mongoose.model('article', ArticleSchema)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
# TODO: Remove once mapping.coffee is refactored out
|
||||
|
||||
Article = require './Article'
|
||||
Handler = require '../commons/Handler'
|
||||
|
||||
ArticleHandler = class ArticleHandler extends Handler
|
||||
modelClass: Article
|
||||
editableProperties: ['body', 'name', 'i18n']
|
||||
jsonSchema: require '../../app/schemas/models/article'
|
||||
editableProperties: Article.schema.editableProperties
|
||||
jsonSchema: Article.schema.jsonSchema
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin() or req.user?.isArtisan()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
log = require 'winston'
|
||||
_ = require 'lodash'
|
||||
|
||||
module.exports.custom = (res, code=500, message='Internal Server Error') ->
|
||||
log.debug "#{code}: #{message}"
|
||||
|
@ -56,3 +57,85 @@ module.exports.clientTimeout = (res, message='The server did not receive the cli
|
|||
log.debug "408: #{message}"
|
||||
res.send 408, message
|
||||
res.end()
|
||||
|
||||
|
||||
# Objects
|
||||
|
||||
errorResponseSchema = {
|
||||
type: 'object'
|
||||
required: ['errorName', 'code', 'message']
|
||||
properties: {
|
||||
error: {
|
||||
description: 'Error object which the callback returned'
|
||||
}
|
||||
errorName: {
|
||||
type: 'string'
|
||||
description: 'Human readable error code name'
|
||||
}
|
||||
code: {
|
||||
type: 'integer'
|
||||
description: 'HTTP error code'
|
||||
}
|
||||
validationErrors: {
|
||||
type: 'array'
|
||||
description: 'TV4 array of validation error objects'
|
||||
}
|
||||
message: {
|
||||
type: 'string'
|
||||
description: 'Human readable descripton of the error'
|
||||
}
|
||||
property: {
|
||||
type: 'string'
|
||||
description: 'Property which is related to the error (conflict, validation).'
|
||||
}
|
||||
}
|
||||
}
|
||||
errorProps = _.keys(errorResponseSchema.properties)
|
||||
|
||||
class NetworkError
|
||||
code: 0
|
||||
|
||||
constructor: (@message, options) ->
|
||||
@stack = (new Error()).stack
|
||||
_.assign(@, options)
|
||||
|
||||
toJSON: ->
|
||||
_.pick(@, errorProps...)
|
||||
|
||||
module.exports.NetworkError = NetworkError
|
||||
|
||||
module.exports.Unauthorized = class Unauthorized extends NetworkError
|
||||
code: 401
|
||||
name: 'Unauthorized'
|
||||
|
||||
module.exports.Forbidden = class Forbidden extends NetworkError
|
||||
code: 403
|
||||
name: 'Forbidden'
|
||||
|
||||
module.exports.NotFound = class NotFound extends NetworkError
|
||||
code: 404
|
||||
name: 'Not Found'
|
||||
|
||||
module.exports.MethodNotAllowed = class MethodNotAllowed extends NetworkError
|
||||
code: 405
|
||||
name: 'Method Not Allowed'
|
||||
|
||||
module.exports.RequestTimeout = class RequestTimeout extends NetworkError
|
||||
code: 407
|
||||
name: 'Request Timeout'
|
||||
|
||||
module.exports.Conflict = class Conflict extends NetworkError
|
||||
code: 409
|
||||
name: 'Conflict'
|
||||
|
||||
module.exports.UnprocessableEntity = class UnprocessableEntity extends NetworkError
|
||||
code: 422
|
||||
name: 'Unprocessable Entity'
|
||||
|
||||
module.exports.InternalServerError = class InternalServerError extends NetworkError
|
||||
code: 500
|
||||
name: 'Internal Server Error'
|
||||
|
||||
module.exports.GatewayTimeout = class GatewayTimeout extends NetworkError
|
||||
code: 504
|
||||
name: 'Gateway Timeout'
|
||||
|
|
|
@ -2,8 +2,12 @@ AnalyticsString = require '../analytics/AnalyticsString'
|
|||
log = require 'winston'
|
||||
mongoose = require 'mongoose'
|
||||
config = require '../../server_config'
|
||||
errors = require '../commons/errors'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
deltasLib = require '../../app/core/deltas'
|
||||
|
||||
module.exports =
|
||||
module.exports = utils =
|
||||
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
||||
|
||||
getCodeCamel: (numWords=3) ->
|
||||
|
@ -92,3 +96,219 @@ module.exports =
|
|||
@analyticsStringCache[str] = document._id
|
||||
return callback @analyticsStringCache[str]
|
||||
insertString()
|
||||
|
||||
getLimitFromReq: (req, options) ->
|
||||
options = _.extend({
|
||||
max: 1000
|
||||
default: 100
|
||||
}, options)
|
||||
|
||||
limit = options.default
|
||||
|
||||
if req.query.limit
|
||||
limit = parseInt(req.query.limit)
|
||||
valid = tv4.validate(limit, {
|
||||
type: 'integer'
|
||||
maximum: options.max
|
||||
minimum: 1
|
||||
})
|
||||
if not valid
|
||||
throw new errors.UnprocessableEntity('Invalid limit parameter.')
|
||||
|
||||
return limit
|
||||
|
||||
|
||||
getSkipFromReq: (req, options) ->
|
||||
options = _.extend({
|
||||
max: 1000000
|
||||
default: 0
|
||||
}, options)
|
||||
|
||||
skip = options.default
|
||||
|
||||
if req.query.skip
|
||||
skip = parseInt(req.query.skip)
|
||||
valid = tv4.validate(skip, {
|
||||
type: 'integer'
|
||||
maximum: options.max
|
||||
minimum: 0
|
||||
})
|
||||
if not valid
|
||||
throw new errors.UnprocessableEntity('Invalid sort parameter.')
|
||||
|
||||
return skip
|
||||
|
||||
|
||||
getProjectFromReq: (req, options) ->
|
||||
options = _.extend({}, options)
|
||||
return null unless req.query.project
|
||||
projection = {}
|
||||
|
||||
if req.query.project is 'true'
|
||||
projection = {original: 1, name: 1, version: 1, description: 1, slug: 1, kind: 1, created: 1, permissions: 1}
|
||||
else
|
||||
for field in req.query.project.split(',')
|
||||
projection[field] = 1
|
||||
|
||||
return projection
|
||||
|
||||
|
||||
applyCustomSearchToDBQ: (req, dbq) ->
|
||||
specialParameters = ['term', 'project', 'conditions']
|
||||
|
||||
return unless req.user?.isAdmin()
|
||||
return unless req.query.filter or req.query.conditions
|
||||
|
||||
# admins can send any sort of query down the wire
|
||||
# Example URL: http://localhost:3000/db/user?filter[anonymous]=true
|
||||
filter = {}
|
||||
if 'filter' of req.query
|
||||
for own key, val of req.query.filter
|
||||
if key not in specialParameters
|
||||
try
|
||||
filter[key] = JSON.parse(val)
|
||||
catch SyntaxError
|
||||
throw new errors.UnprocessableEntity("Could not parse filter for key '#{key}'.")
|
||||
dbq.find(filter)
|
||||
|
||||
# Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated')
|
||||
# Example URL: http://localhost:3000/db/user?conditions[limit]=20&conditions[sort]="-dateCreated"
|
||||
for own key, val of req.query.conditions
|
||||
if not dbq[key]
|
||||
throw new errors.UnprocessableEntity("No query condition '#{key}'.")
|
||||
try
|
||||
val = JSON.parse(val)
|
||||
dbq[key](val)
|
||||
catch SyntaxError
|
||||
throw new errors.UnprocessableEntity("Could not parse condition for key '#{key}'.")
|
||||
|
||||
|
||||
viewSearch: (dbq, req, done) ->
|
||||
Model = dbq.model
|
||||
# TODO: Make this function only alter dbq or returns a find. It should not also execute the query.
|
||||
term = req.query.term
|
||||
matchedObjects = []
|
||||
filters = if Model.schema.uses_coco_versions or Model.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
|
||||
|
||||
if Model.schema.uses_coco_permissions and req.user
|
||||
filters.push {filter: {index: req.user.get('id')}}
|
||||
|
||||
for filter in filters
|
||||
callback = (err, results) ->
|
||||
return done(new errors.InternalServerError('Error fetching search results.', {err: err})) if err
|
||||
for r in results.results ? results
|
||||
obj = r.obj ? r
|
||||
continue if obj in matchedObjects # TODO: probably need a better equality check
|
||||
matchedObjects.push obj
|
||||
filters.pop() # doesn't matter which one
|
||||
unless filters.length
|
||||
done(null, matchedObjects)
|
||||
|
||||
if term
|
||||
filter.filter.$text = $search: term
|
||||
else if filters.length is 1 and filters[0].filter?.index is true
|
||||
# All we are doing is an empty text search, but that doesn't hit the index,
|
||||
# so we'll just look for the slug.
|
||||
filter.filter = slug: {$exists: true}
|
||||
|
||||
# This try/catch is here to handle when a custom search tries to find by slug. TODO: Fix this more gracefully.
|
||||
try
|
||||
dbq.find filter.filter
|
||||
catch
|
||||
dbq.exec callback
|
||||
|
||||
|
||||
assignBody: (req, doc, options={}) ->
|
||||
if _.isEmpty(req.body)
|
||||
throw new errors.UnprocessableEntity('No input')
|
||||
|
||||
props = doc.schema.editableProperties.slice()
|
||||
|
||||
if doc.isNew
|
||||
props = props.concat doc.schema.postEditableProperties
|
||||
|
||||
if doc.schema.uses_coco_permissions and req.user
|
||||
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'
|
||||
if doc.isNew or isOwner or req.user?.isAdmin()
|
||||
props.push 'permissions'
|
||||
|
||||
props.push 'commitMessage' if doc.schema.uses_coco_versions
|
||||
props.push 'allowPatches' if doc.schema.is_patchable
|
||||
|
||||
for prop in props
|
||||
if (val = req.body[prop])?
|
||||
doc.set prop, val
|
||||
else if options.unsetMissing and doc.get(prop)?
|
||||
doc.set prop, undefined
|
||||
|
||||
|
||||
validateDoc: (doc) ->
|
||||
obj = doc.toObject()
|
||||
# Hack to get saving of Users to work. Probably should replace these props with strings
|
||||
# so that validation doesn't get hung up on Date objects in the documents.
|
||||
delete obj.dateCreated
|
||||
tv4 = require('tv4').tv4
|
||||
result = tv4.validateMultiple(obj, doc.schema.jsonSchema)
|
||||
if not result.valid
|
||||
throw new errors.UnprocessableEntity('JSON-schema validation failed', { validationErrors: result.errors })
|
||||
|
||||
|
||||
getDocFromHandle: (req, Model, options, done) ->
|
||||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
|
||||
dbq = Model.find()
|
||||
handle = req.params.handle
|
||||
if not handle
|
||||
return done(new errors.UnprocessableEntity('No handle provided.'))
|
||||
if utils.isID(handle)
|
||||
dbq.findOne({ _id: handle })
|
||||
else
|
||||
dbq.findOne({ slug: handle })
|
||||
|
||||
dbq.exec(done)
|
||||
|
||||
|
||||
initDoc: (req, Model) ->
|
||||
# TODO: Move to model superclass or plugins?
|
||||
doc = new Model({})
|
||||
|
||||
if Model.schema.is_patchable
|
||||
watchers = [req.user.get('_id')]
|
||||
if req.user.isAdmin() # https://github.com/codecombat/codecombat/issues/1105
|
||||
nick = mongoose.Types.ObjectId('512ef4805a67a8c507000001')
|
||||
watchers.push nick unless _.find watchers, (id) -> id.equals nick
|
||||
doc.set 'watchers', watchers
|
||||
|
||||
if Model.schema.uses_coco_versions
|
||||
doc.set('original', doc._id)
|
||||
doc.set('creator', req.user._id)
|
||||
|
||||
hasAccessToDocument: (req, doc, method) ->
|
||||
method = method or req.method
|
||||
return true if req.user?.isAdmin()
|
||||
|
||||
if doc.schema.uses_coco_translation_coverage and method in ['post', 'put']
|
||||
return true if @isJustFillingTranslations(req, doc)
|
||||
|
||||
if doc.schema.uses_coco_permissions
|
||||
return doc.hasPermissionsForMethod?(req.user, method)
|
||||
return true
|
||||
|
||||
isJustFillingTranslations: (req, doc) ->
|
||||
differ = deltasLib.makeJSONDiffer()
|
||||
omissions = ['original'].concat(deltasLib.DOC_SKIP_PATHS)
|
||||
delta = differ.diff(_.omit(doc.toObject(), omissions), _.omit(req.body, omissions))
|
||||
flattened = deltasLib.flattenDelta(delta)
|
||||
_.all flattened, (delta) ->
|
||||
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
|
||||
return false unless _.isArray(delta.o)
|
||||
return true if 'i18nCoverage' in delta.dataPath
|
||||
return false unless delta.o.length is 1
|
||||
index = delta.deltaPath.indexOf('i18n')
|
||||
return false if index is -1
|
||||
return false if delta.deltaPath[index+1] in ['en', 'en-US', 'en-GB'] # English speakers are most likely just spamming, so always treat those as patches, not saves.
|
||||
return true
|
||||
|
||||
Promise.promisifyAll(module.exports)
|
31
server/middleware/auth.coffee
Normal file
31
server/middleware/auth.coffee
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Middleware for both authentication and authorization
|
||||
|
||||
errors = require '../commons/errors'
|
||||
|
||||
module.exports = {
|
||||
checkDocumentPermissions: (req, res, next) ->
|
||||
return next() if req.user?.isAdmin()
|
||||
if not req.doc.hasPermissionsForMethod(req.user, req.method)
|
||||
if req.user
|
||||
return next new errors.Forbidden('You do not have permissions necessary.')
|
||||
return next new errors.Unauthorized('You must be logged in.')
|
||||
next()
|
||||
|
||||
checkLoggedIn: ->
|
||||
return (req, res, next) ->
|
||||
if not req.user
|
||||
return next new errors.Unauthorized('You must be logged in.')
|
||||
next()
|
||||
|
||||
checkHasPermission: (permissions) ->
|
||||
if _.isString(permissions)
|
||||
permissions = [permissions]
|
||||
|
||||
return (req, res, next) ->
|
||||
if not req.user
|
||||
return next new errors.Unauthorized('You must be logged in.')
|
||||
if not _.size(_.intersection(req.user.get('permissions'), permissions))
|
||||
return next new errors.Forbidden('You do not have permissions necessary.')
|
||||
next()
|
||||
|
||||
}
|
20
server/middleware/files.coffee
Normal file
20
server/middleware/files.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Grid = require 'gridfs-stream'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
module.exports =
|
||||
files: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not doc
|
||||
throw new errors.NotFound('Document not found.')
|
||||
module = options.module or req.path[4..].split('/')[0]
|
||||
query = { 'metadata.path': "db/#{module}/#{doc.id}" }
|
||||
|
||||
c = Grid.gfs.collection('media')
|
||||
c.findAsync = Promise.promisify(c.find)
|
||||
cursor = yield c.findAsync(query)
|
||||
cursor.toArrayAsync = Promise.promisify(cursor.toArray)
|
||||
files = yield cursor.toArrayAsync()
|
||||
res.status(200).send(files)
|
7
server/middleware/index.coffee
Normal file
7
server/middleware/index.coffee
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports =
|
||||
auth: require './auth'
|
||||
files: require './files'
|
||||
named: require './named'
|
||||
patchable: require './patchable'
|
||||
rest: require './rest'
|
||||
versions: require './versions'
|
31
server/middleware/named.coffee
Normal file
31
server/middleware/named.coffee
Normal file
|
@ -0,0 +1,31 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
module.exports =
|
||||
names: (Model, options={}) -> wrap (req, res) ->
|
||||
# TODO: migrate to /db/collection?ids=...&project=... and /db/collection?originals=...&project=...
|
||||
|
||||
ids = req.query.ids or req.body.ids
|
||||
ids = ids.split(',') if _.isString ids
|
||||
ids = _.uniq ids
|
||||
|
||||
# Hack: levels loading thang types need the components returned as well.
|
||||
# Need a way to specify a projection for a query.
|
||||
project = {name: 1, original: 1, kind: 1, components: 1, prerenderedSpriteSheetData: 1}
|
||||
sort = if Model.schema.uses_coco_versions then {'version.major': -1, 'version.minor': -1} else {}
|
||||
|
||||
for id in ids
|
||||
if not utils.isID(id)
|
||||
throw new errors.UnprocessableEntity('Invalid MongoDB id given')
|
||||
|
||||
ids = (mongoose.Types.ObjectId(id) for id in ids)
|
||||
|
||||
promises = []
|
||||
for id in ids
|
||||
q = if Model.schema.uses_coco_versions then { original: id } else { _id: id }
|
||||
promises.push Model.findOne(q).select(project).sort(sort).exec()
|
||||
|
||||
documents = yield promises
|
||||
res.status(200).send(documents)
|
50
server/middleware/patchable.coffee
Normal file
50
server/middleware/patchable.coffee
Normal file
|
@ -0,0 +1,50 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
Patch = require '../models/Patch'
|
||||
|
||||
module.exports =
|
||||
patches: (options={}) -> wrap (req, res) ->
|
||||
dbq = Patch.find()
|
||||
dbq.limit(utils.getLimitFromReq(req))
|
||||
dbq.skip(utils.getSkipFromReq(req))
|
||||
dbq.select(utils.getProjectFromReq(req))
|
||||
|
||||
id = req.params.handle
|
||||
if not utils.isID(id)
|
||||
throw new errors.UnprocessableEntity('Invalid ID')
|
||||
|
||||
query =
|
||||
$or: [
|
||||
{'target.original': id+''}
|
||||
{'target.original': mongoose.Types.ObjectId(id)}
|
||||
]
|
||||
status: req.query.status or 'pending'
|
||||
|
||||
patches = yield dbq.find(query).sort('-created')
|
||||
res.status(200).send(patches)
|
||||
|
||||
joinWatchers: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not doc
|
||||
throw new errors.NotFound('Document not found.')
|
||||
if not utils.hasAccessToDocument(req, doc, 'get')
|
||||
throw new errors.Forbidden()
|
||||
updateResult = yield doc.update({ $addToSet: { watchers: req.user.get('_id') }})
|
||||
if updateResult.nModified
|
||||
watchers = doc.get('watchers')
|
||||
watchers.push(req.user.get('_id'))
|
||||
doc.set('watchers', watchers)
|
||||
res.status(200).send(doc)
|
||||
|
||||
leaveWatchers: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not doc
|
||||
throw new errors.NotFound('Document not found.')
|
||||
updateResult = yield doc.update({ $pull: { watchers: req.user.get('_id') }})
|
||||
if updateResult.nModified
|
||||
watchers = doc.get('watchers')
|
||||
watchers = _.filter watchers, (id) -> not id.equals(req.user._id)
|
||||
doc.set('watchers', watchers)
|
||||
res.status(200).send(doc)
|
40
server/middleware/rest.coffee
Normal file
40
server/middleware/rest.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
|
||||
module.exports =
|
||||
get: (Model, options={}) -> wrap (req, res) ->
|
||||
dbq = Model.find()
|
||||
dbq.limit(utils.getLimitFromReq(req))
|
||||
dbq.skip(utils.getSkipFromReq(req))
|
||||
dbq.select(utils.getProjectFromReq(req))
|
||||
utils.applyCustomSearchToDBQ(req, dbq)
|
||||
|
||||
if Model.schema.uses_coco_translation_coverage and req.query.view is 'i18n-coverage'
|
||||
dbq.find({ slug: {$exists: true}, i18nCoverage: {$exists: true} })
|
||||
|
||||
results = yield utils.viewSearchAsync(dbq, req)
|
||||
res.send(results)
|
||||
|
||||
post: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = utils.initDoc(req, Model)
|
||||
utils.assignBody(req, doc)
|
||||
utils.validateDoc(doc)
|
||||
doc = yield doc.save()
|
||||
res.status(201).send(doc.toObject())
|
||||
|
||||
getByHandle: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not doc
|
||||
throw new errors.NotFound('Document not found.')
|
||||
res.status(200).send(doc.toObject())
|
||||
|
||||
put: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not doc
|
||||
throw new errors.NotFound('Document not found.')
|
||||
|
||||
utils.assignBody(req, doc)
|
||||
utils.validateDoc(doc)
|
||||
doc = yield doc.save()
|
||||
res.status(200).send(doc.toObject())
|
168
server/middleware/versions.coffee
Normal file
168
server/middleware/versions.coffee
Normal file
|
@ -0,0 +1,168 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
User = require '../users/User'
|
||||
sendwithus = require '../sendwithus'
|
||||
hipchat = require '../hipchat'
|
||||
_ = require 'lodash'
|
||||
wrap = require 'co-express'
|
||||
mongoose = require 'mongoose'
|
||||
|
||||
module.exports =
|
||||
postNewVersion: (Model, options={}) -> wrap (req, res) ->
|
||||
parent = yield utils.getDocFromHandleAsync(req, Model)
|
||||
if not parent
|
||||
throw new errors.NotFound('Parent not found.')
|
||||
|
||||
# TODO: Figure out a better way to do this
|
||||
if options.hasPermissionsOrTranslations
|
||||
permissions = options.hasPermissionsOrTranslations
|
||||
permissions = [permissions] if _.isString(permissions)
|
||||
permissions = ['admin'] if not _.isArray(permissions)
|
||||
hasPermission = _.any(req.user?.hasPermission(permission) for permission in permissions)
|
||||
if not (hasPermission or utils.isJustFillingTranslations(req, parent))
|
||||
throw new errors.Forbidden()
|
||||
|
||||
doc = utils.initDoc(req, Model)
|
||||
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
||||
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
||||
|
||||
utils.assignBody(req, doc, { unsetMissing: true })
|
||||
|
||||
# Get latest version
|
||||
major = req.body.version?.major
|
||||
original = parent.get('original')
|
||||
if _.isNumber(major)
|
||||
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
|
||||
else
|
||||
q1 = Model.findOne({original: original, 'version.isLatestMajor': true})
|
||||
q1.select 'version'
|
||||
latest = yield q1.exec()
|
||||
|
||||
if not latest
|
||||
# handle the case where no version is marked as latest, since making new
|
||||
# versions is not atomic
|
||||
if _.isNumber(major)
|
||||
q2 = Model.findOne({original: original, 'version.major': major})
|
||||
q2.sort({'version.minor': -1})
|
||||
else
|
||||
q2 = Model.findOne()
|
||||
q2.sort({'version.major': -1, 'version.minor': -1})
|
||||
q2.select 'version'
|
||||
latest = yield q2.exec()
|
||||
if not latest
|
||||
throw new errors.NotFound('Previous version not found.')
|
||||
|
||||
# Transfer latest version
|
||||
major = req.body.version?.major
|
||||
version = _.clone(latest.get('version'))
|
||||
wasLatestMajor = version.isLatestMajor
|
||||
version.isLatestMajor = false
|
||||
if _.isNumber(major)
|
||||
version.isLatestMinor = false
|
||||
|
||||
conditions = {_id: latest._id}
|
||||
|
||||
raw = yield Model.update(conditions, {version: version, $unset: {index: 1, slug: 1}})
|
||||
if not raw.nModified
|
||||
console.error('Conditions', conditions)
|
||||
console.error('Doc', doc)
|
||||
console.error('Raw response', raw)
|
||||
throw new errors.InternalServerError('Latest version could not be modified.')
|
||||
|
||||
# update the new doc with version, index information
|
||||
# Relying heavily on Mongoose schema default behavior here. TODO: Make explicit?
|
||||
if _.isNumber(major)
|
||||
doc.set({
|
||||
'version.major': latest.version.major
|
||||
'version.minor': latest.version.minor + 1
|
||||
'version.isLatestMajor': wasLatestMajor
|
||||
})
|
||||
if wasLatestMajor
|
||||
doc.set('index', true)
|
||||
else
|
||||
doc.set({index: undefined, slug: undefined})
|
||||
else
|
||||
doc.set('version.major', latest.version.major + 1)
|
||||
doc.set('index', true)
|
||||
|
||||
doc.set('parent', latest._id)
|
||||
|
||||
doc = yield doc.save()
|
||||
|
||||
editPath = req.headers['x-current-path']
|
||||
docLink = "http://codecombat.com#{editPath}"
|
||||
|
||||
# Post a message on HipChat
|
||||
message = "#{req.user.get('name')} saved a change to <a href=\"#{docLink}\">#{doc.get('name')}</a>: #{doc.get('commitMessage') or '(no commit message)'}"
|
||||
rooms = if /Diplomat submission/.test(message) then ['main'] else ['main', 'artisans']
|
||||
hipchat.sendHipChatMessage message, rooms
|
||||
|
||||
# Send emails to watchers
|
||||
watchers = doc.get('watchers') or []
|
||||
# Don't send these emails to the person who submitted the patch, or to Nick, George, or Scott.
|
||||
watchers = (w for w in watchers when not w.equals(req.user.get('_id')) and not (w + '' in ['512ef4805a67a8c507000001', '5162fab9c92b4c751e000274', '51538fdb812dd9af02000001']))
|
||||
if watchers.length
|
||||
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) ->
|
||||
for watcher in watchers
|
||||
context =
|
||||
email_id: sendwithus.templates.change_made_notify_watcher
|
||||
recipient:
|
||||
address: watcher.get('email')
|
||||
name: watcher.get('name')
|
||||
email_data:
|
||||
doc_name: doc.get('name') or '???'
|
||||
submitter_name: req.user.get('name') or '???'
|
||||
doc_link: if editPath then docLink else null
|
||||
commit_message: doc.get('commitMessage')
|
||||
sendwithus.api.send context, _.noop
|
||||
|
||||
res.status(201).send(doc.toObject())
|
||||
|
||||
|
||||
|
||||
getLatestVersion: (Model, options={}) -> wrap (req, res) ->
|
||||
# can get latest overall version, latest of a major version, or a specific version
|
||||
original = req.params.handle
|
||||
version = req.params.version
|
||||
if not utils.isID(original)
|
||||
throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original)
|
||||
|
||||
query = { 'original': mongoose.Types.ObjectId(original) }
|
||||
if version?
|
||||
version = version.split('.')
|
||||
majorVersion = parseInt(version[0])
|
||||
minorVersion = parseInt(version[1])
|
||||
query['version.major'] = majorVersion unless _.isNaN(majorVersion)
|
||||
query['version.minor'] = minorVersion unless _.isNaN(minorVersion)
|
||||
dbq = Model.findOne(query)
|
||||
|
||||
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
||||
|
||||
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
|
||||
projection = utils.getProjectFromReq(req)
|
||||
if projection
|
||||
extraProjectionProps = []
|
||||
extraProjectionProps.push 'permissions' unless projection.permissions
|
||||
extraProjectionProps.push 'version' unless projection.version
|
||||
projection.permissions = 1
|
||||
projection.version = 1
|
||||
dbq.select(projection)
|
||||
|
||||
doc = yield dbq.exec()
|
||||
throw new errors.NotFound() if not doc
|
||||
throw new errors.Forbidden() unless utils.hasAccessToDocument(req, doc)
|
||||
doc = _.omit doc, extraProjectionProps if extraProjectionProps?
|
||||
|
||||
res.status(200).send(doc.toObject())
|
||||
|
||||
|
||||
versions: (Model, options={}) -> wrap (req, res) ->
|
||||
original = req.params.handle
|
||||
dbq = Model.find({'original': mongoose.Types.ObjectId(original)})
|
||||
dbq.sort({'created': -1})
|
||||
dbq.limit(utils.getLimitFromReq(req))
|
||||
dbq.skip(utils.getSkipFromReq(req))
|
||||
dbq.select(utils.getProjectFromReq(req) or 'slug name version commitMessage created creator permissions')
|
||||
|
||||
results = yield dbq.exec()
|
||||
res.status(200).send(results)
|
3
server/models/Article.coffee
Normal file
3
server/models/Article.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate Article to here
|
||||
|
||||
module.exports = require '../articles/Article'
|
3
server/models/Patch.coffee
Normal file
3
server/models/Patch.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate Patch to here
|
||||
|
||||
module.exports = require '../patches/Patch'
|
|
@ -179,7 +179,7 @@ module.exports.VersionedPlugin = (schema) ->
|
|||
latest = latest[0]
|
||||
|
||||
# don't fix missing versions by default. In all likelihood, it's about to change anyway
|
||||
if options.autofix
|
||||
if options.autofix # not used
|
||||
latest.version.isLatestMajor = true
|
||||
latest.version.isLatestMinor = true
|
||||
latestObject = latest.toObject()
|
||||
|
@ -204,7 +204,7 @@ module.exports.VersionedPlugin = (schema) ->
|
|||
return done(null, null) if latest.length is 0
|
||||
latest = latest[0]
|
||||
|
||||
if options.autofix
|
||||
if options.autofix # not used
|
||||
latestObject = latest.toObject()
|
||||
latestObject.version.isLatestMajor = true
|
||||
latestObject.version.isLatestMinor = true
|
||||
|
|
|
@ -1,4 +1,22 @@
|
|||
mw = require '../middleware'
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
Article = require '../models/Article'
|
||||
app.get('/db/article', mw.rest.get(Article))
|
||||
app.post('/db/article', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article))
|
||||
app.get('/db/article/names', mw.named.names(Article))
|
||||
app.post('/db/article/names', mw.named.names(Article))
|
||||
app.get('/db/article/:handle', mw.rest.getByHandle(Article))
|
||||
app.put('/db/article/:handle', mw.auth.checkHasPermission(['admin']), mw.rest.put(Article))
|
||||
app.patch('/db/article/:handle', mw.auth.checkHasPermission(['admin']), mw.rest.put(Article))
|
||||
app.post('/db/article/:handle/new-version', mw.auth.checkLoggedIn(), mw.versions.postNewVersion(Article, { hasPermissionsOrTranslations: 'artisan' }))
|
||||
app.get('/db/article/:handle/versions', mw.versions.versions(Article))
|
||||
app.get('/db/article/:handle/version/?(:version)?', mw.versions.getLatestVersion(Article))
|
||||
app.get('/db/article/:handle/files', mw.files.files(Article, {module: 'article'}))
|
||||
app.get('/db/article/:handle/patches', mw.patchable.patches(Article))
|
||||
app.post('/db/article/:handle/watchers', mw.patchable.joinWatchers(Article))
|
||||
app.delete('/db/article/:handle/watchers', mw.patchable.leaveWatchers(Article))
|
||||
|
||||
app.get '/db/products', require('./db/product').get
|
||||
|
||||
app.get '/healthcheck', (req, res) ->
|
||||
|
|
|
@ -45,6 +45,14 @@ UserSchema.methods.isInGodMode = ->
|
|||
UserSchema.methods.isAdmin = ->
|
||||
p = @get('permissions')
|
||||
return p and 'admin' in p
|
||||
|
||||
UserSchema.methods.hasPermission = (neededPermissions) ->
|
||||
permissions = @get('permissions') or []
|
||||
if _.contains(permissions, 'admin')
|
||||
return true
|
||||
if _.isString(neededPermissions)
|
||||
neededPermissions = [neededPermissions]
|
||||
return _.size(_.intersection(permissions, neededPermissions))
|
||||
|
||||
UserSchema.methods.isArtisan = ->
|
||||
p = @get('permissions')
|
||||
|
|
|
@ -20,6 +20,7 @@ hipchat = require './server/hipchat'
|
|||
global.tv4 = require 'tv4' # required for TreemaUtils to work
|
||||
global.jsondiffpatch = require 'jsondiffpatch'
|
||||
global.stripe = require('stripe')(config.stripe.secretKey)
|
||||
errors = require './server/commons/errors'
|
||||
|
||||
|
||||
productionLogging = (tokens, req, res) ->
|
||||
|
@ -48,9 +49,21 @@ developmentLogging = (tokens, req, res) ->
|
|||
setupErrorMiddleware = (app) ->
|
||||
app.use (err, req, res, next) ->
|
||||
if err
|
||||
if err.name is 'MongoError' and err.code is 11000
|
||||
err = new errors.Conflict('MongoDB conflict error.')
|
||||
if err.code is 422 and err.response
|
||||
err = new errors.UnprocessableEntity(err.response)
|
||||
if err.code is 409 and err.response
|
||||
err = new errors.Conflict(err.response)
|
||||
|
||||
# TODO: Make all errors use this
|
||||
if err instanceof errors.NetworkError
|
||||
return res.status(err.code).send(err.toJSON())
|
||||
|
||||
if err.status and 400 <= err.status < 500
|
||||
res.status(err.status).send("Error #{err.status}")
|
||||
return
|
||||
|
||||
res.status(err.status ? 500).send(error: "Something went wrong!")
|
||||
message = "Express error: #{req.method} #{req.path}: #{err.message}"
|
||||
log.error "#{message}, stack: #{err.stack}"
|
||||
|
|
|
@ -71,6 +71,8 @@ GLOBAL.saveModels = (models, done) ->
|
|||
GLOBAL.simplePermissions = [target: 'public', access: 'owner']
|
||||
GLOBAL.ObjectId = mongoose.Types.ObjectId
|
||||
GLOBAL.request = require 'request'
|
||||
Promise = require 'bluebird'
|
||||
Promise.promisifyAll(request, {multiArgs: true})
|
||||
|
||||
GLOBAL.unittest = {}
|
||||
unittest.users = unittest.users or {}
|
||||
|
|
|
@ -1,138 +1,679 @@
|
|||
require '../common'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
requestAsync = Promise.promisify(request, {multiArgs: true})
|
||||
|
||||
describe '/db/article', ->
|
||||
request = require 'request'
|
||||
it 'clears the db first', (done) ->
|
||||
clearModels [User, Article], (err) ->
|
||||
throw err if err
|
||||
describe 'GET /db/article', ->
|
||||
articleData1 = { name: 'Article 1', body: 'Article 1 body cow', i18nCoverage: [] }
|
||||
articleData2 = { name: 'Article 2', body: 'Article 2 body moo' }
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
yield request.postAsync(getURL('/db/article'), { json: articleData1 })
|
||||
yield request.postAsync(getURL('/db/article'), { json: articleData2 })
|
||||
yield utils.logoutAsync()
|
||||
done()
|
||||
|
||||
|
||||
it 'returns an array of Article objects', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL('/db/article'), json: true }
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
||||
|
||||
|
||||
it 'accepts a limit parameter', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?limit=1'), json: true}
|
||||
expect(body.length).toBe(1)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns 422 for an invalid limit parameter', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?limit=word'), json: true}
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
it 'accepts a skip parameter', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?skip=1'), json: true}
|
||||
expect(body.length).toBe(1)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?skip=2'), json: true}
|
||||
expect(body.length).toBe(0)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns 422 for an invalid skip parameter', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?skip=???'), json: true}
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
it 'accepts a custom project parameter', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?project=name,body'), json: true}
|
||||
expect(body.length).toBe(2)
|
||||
for doc in body
|
||||
expect(_.size(_.xor(_.keys(doc), ['_id', 'name', 'body']))).toBe(0)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns a default projection if project is "true"', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?project=true'), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
expect(body[0].body).toBeUndefined()
|
||||
expect(body[0].version).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
it 'accepts custom filter parameters', utils.wrap (done) ->
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?filter[slug]="article-1"'), json: true}
|
||||
expect(body.length).toBe(1)
|
||||
done()
|
||||
|
||||
|
||||
it 'ignores custom filter parameters for non-admins', utils.wrap (done) ->
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?filter[slug]="article-1"'), json: true}
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
||||
|
||||
|
||||
it 'accepts custom condition parameters', utils.wrap (done) ->
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?conditions[select]="slug body"'), json: true}
|
||||
expect(body.length).toBe(2)
|
||||
for doc in body
|
||||
expect(_.size(_.xor(_.keys(doc), ['_id', 'slug', 'body']))).toBe(0)
|
||||
done()
|
||||
|
||||
|
||||
it 'ignores custom condition parameters for non-admins', utils.wrap (done) ->
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?conditions[select]="slug body"'), json: true}
|
||||
expect(body.length).toBe(2)
|
||||
for doc in body
|
||||
expect(doc.name).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
it 'allows non-admins to view by i18n-coverage', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?view=i18n-coverage'), json: true}
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].slug).toBe('article-1')
|
||||
done()
|
||||
|
||||
|
||||
it 'allows non-admins to search by text', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/db/article?term=moo'), json: true}
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].slug).toBe('article-2')
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/article', ->
|
||||
|
||||
articleData = { name: 'Article', body: 'Article', otherProp: 'not getting set' }
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[@res, @body] = yield request.postAsync {
|
||||
uri: getURL('/db/article'), json: articleData
|
||||
}
|
||||
done()
|
||||
|
||||
|
||||
it 'creates a new Article, returning 201', utils.wrap (done) ->
|
||||
expect(@res.statusCode).toBe(201)
|
||||
article = yield Article.findById(@body._id).exec()
|
||||
expect(article).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
it 'sets creator to the user who created it', ->
|
||||
expect(@res.body.creator).toBe(@admin.id)
|
||||
|
||||
|
||||
it 'sets original to _id', ->
|
||||
body = @res.body
|
||||
expect(body.original).toBe(body._id)
|
||||
|
||||
|
||||
it 'returns 422 when no input is provided', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article') }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
it 'allows you to set Article\'s editableProperties', ->
|
||||
expect(@body.name).toBe('Article')
|
||||
|
||||
|
||||
it 'ignores properties not included in editableProperties', ->
|
||||
expect(@body.otherProp).toBeUndefined()
|
||||
|
||||
|
||||
it 'returns 422 when properties do not pass validation', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync {
|
||||
uri: getURL('/db/article'), json: { i18nCoverage: 9001 }
|
||||
}
|
||||
expect(res.statusCode).toBe(422)
|
||||
expect(body.validationErrors).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
it 'allows admins to create Articles', -> # handled in beforeEach
|
||||
|
||||
|
||||
it 'allows artisans to create Articles', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
artisan = yield utils.initArtisanAsync({})
|
||||
yield utils.loginUserAsync(artisan)
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
|
||||
expect(res.statusCode).toBe(201)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not allow normal users to create Articles', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
user = yield utils.initUserAsync({})
|
||||
yield utils.loginUserAsync(user)
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not allow anonymous users to create Articles', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
yield utils.logoutAsync()
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not allow creating Articles with reserved words', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: { name: 'Names' } }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not allow creating a second article of the same name', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(409)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/article/:handle', ->
|
||||
|
||||
articleData = { name: 'Some Name', body: 'Article' }
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[@res, @body] = yield request.postAsync {
|
||||
uri: getURL('/db/article'), json: articleData
|
||||
}
|
||||
done()
|
||||
|
||||
|
||||
it 'returns Article by id', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/article/#{@body._id}"), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(_.isObject(body)).toBe(true)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns Article by slug', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/article/some-name"), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(_.isObject(body)).toBe(true)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns not found if handle does not exist in the db', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/article/dne"), json: true}
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
||||
|
||||
|
||||
putTests = (method='PUT') ->
|
||||
articleData = { name: 'Some Name', body: 'Article' }
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[@res, @body] = yield request.postAsync {
|
||||
uri: getURL('/db/article'), json: articleData
|
||||
}
|
||||
done()
|
||||
|
||||
|
||||
it 'edits editable Article properties', utils.wrap (done) ->
|
||||
[res, body] = yield requestAsync {method: method, uri: getURL("/db/article/#{@body._id}"), json: { body: 'New body' }}
|
||||
expect(body.body).toBe('New body')
|
||||
done()
|
||||
|
||||
|
||||
it 'updates the slug when the name is changed', utils.wrap (done) ->
|
||||
[res, body] = yield requestAsync {method: method, uri: getURL("/db/article/#{@body._id}"), json: json = { name: 'New name' }}
|
||||
expect(body.name).toBe('New name')
|
||||
expect(body.slug).toBe('new-name')
|
||||
done()
|
||||
|
||||
|
||||
it 'does not allow normal artisan, non-admins to make changes', utils.wrap (done) ->
|
||||
artisan = yield utils.initArtisanAsync({})
|
||||
yield utils.loginUserAsync(artisan)
|
||||
[res, body] = yield requestAsync {method: method, uri: getURL("/db/article/#{@body._id}"), json: { name: 'Another name' }}
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
describe 'PUT /db/article/:handle', -> putTests('PUT')
|
||||
describe 'PATCH /db/article/:handle', -> putTests('PATCH')
|
||||
|
||||
|
||||
describe 'POST /db/article/:handle/new-version', ->
|
||||
articleData = { name: 'Article name', body: 'Article body', i18n: {} }
|
||||
articleID = null
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
articleID = body._id
|
||||
done()
|
||||
|
||||
postNewVersion = Promise.promisify (json, expectedStatus=201, done) ->
|
||||
if _.isFunction(expectedStatus)
|
||||
done = expectedStatus
|
||||
expectedStatus = 201
|
||||
url = getURL("/db/article/#{articleID}/new-version")
|
||||
request.post { uri: url, json: json }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(expectedStatus)
|
||||
done(err)
|
||||
|
||||
testArrayEqual = (given, expected) ->
|
||||
expect(_.isEqual(given, expected)).toBe(true)
|
||||
|
||||
|
||||
|
||||
it 'creates a new major version, updating model and version properties', utils.wrap (done) ->
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' })
|
||||
yield postNewVersion({ name: 'New name', body: 'New new body' })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(3)
|
||||
versions = (article.get('version') for article in articles)
|
||||
articles = (article.toObject() for article in articles)
|
||||
|
||||
testArrayEqual(_.pluck(versions, 'major'), [0, 1, 2])
|
||||
testArrayEqual(_.pluck(versions, 'minor'), [0, 0, 0])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMajor'), [false, false, true])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMinor'), [true, true, true])
|
||||
testArrayEqual(_.pluck(articles, 'name'), ['Article name', 'Article name', 'New name'])
|
||||
testArrayEqual(_.pluck(articles, 'body'), ['Article body', 'New body', 'New new body'])
|
||||
testArrayEqual(_.pluck(articles, 'slug'), [undefined, undefined, 'new-name'])
|
||||
testArrayEqual(_.pluck(articles, 'index'), [undefined, undefined, true])
|
||||
done()
|
||||
|
||||
|
||||
it 'works if there is no document with the appropriate version settings (new major)', utils.wrap (done) ->
|
||||
article = yield Article.findById(articleID)
|
||||
article.set({ 'version.isLatestMajor': false, 'version.isLatestMinor': false })
|
||||
yield article.save()
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(2)
|
||||
|
||||
versions = (article.get('version') for article in articles)
|
||||
articles = (article.toObject() for article in articles)
|
||||
|
||||
testArrayEqual(_.pluck(versions, 'major'), [0, 1])
|
||||
testArrayEqual(_.pluck(versions, 'minor'), [0, 0])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMajor'), [false, true])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMinor'), [false, true]) # does not fix the old version's value
|
||||
testArrayEqual(_.pluck(articles, 'body'), ['Article body', 'New body'])
|
||||
testArrayEqual(_.pluck(articles, 'slug'), [undefined, 'article-name'])
|
||||
testArrayEqual(_.pluck(articles, 'index'), [undefined, true])
|
||||
done()
|
||||
|
||||
|
||||
it 'creates a new minor version if version.major is included', utils.wrap (done) ->
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body', version: { major: 0 } })
|
||||
yield postNewVersion({ name: 'Article name', body: 'New new body', version: { major: 0 } })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(3)
|
||||
|
||||
versions = (article.get('version') for article in articles)
|
||||
articles = (article.toObject() for article in articles)
|
||||
|
||||
testArrayEqual(_.pluck(versions, 'major'), [0, 0, 0])
|
||||
testArrayEqual(_.pluck(versions, 'minor'), [0, 1, 2])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMajor'), [false, false, true])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMinor'), [false, false, true])
|
||||
testArrayEqual(_.pluck(articles, 'name'), ['Article name', 'Article name', 'Article name'])
|
||||
testArrayEqual(_.pluck(articles, 'body'), ['Article body', 'New body', 'New new body'])
|
||||
testArrayEqual(_.pluck(articles, 'slug'), [undefined, undefined, 'article-name'])
|
||||
testArrayEqual(_.pluck(articles, 'index'), [undefined, undefined, true])
|
||||
done()
|
||||
|
||||
|
||||
it 'works if there is no document with the appropriate version settings (new minor)', utils.wrap (done) ->
|
||||
article = yield Article.findById(articleID)
|
||||
article.set({ 'version.isLatestMajor': false, 'version.isLatestMinor': false })
|
||||
yield article.save()
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body', version: { major: 0 } })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(2)
|
||||
|
||||
versions = (article.get('version') for article in articles)
|
||||
articles = (article.toObject() for article in articles)
|
||||
|
||||
testArrayEqual(_.pluck(versions, 'major'), [0, 0])
|
||||
testArrayEqual(_.pluck(versions, 'minor'), [0, 1])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMajor'), [false, false])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMinor'), [false, true])
|
||||
testArrayEqual(_.pluck(articles, 'body'), ['Article body', 'New body'])
|
||||
testArrayEqual(_.pluck(articles, 'slug'), [undefined, 'article-name'])
|
||||
testArrayEqual(_.pluck(articles, 'index'), [undefined, true])
|
||||
done()
|
||||
|
||||
|
||||
it 'allows adding new minor versions to old major versions', utils.wrap (done) ->
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' })
|
||||
yield postNewVersion({ name: 'Article name', body: 'New new body', version: { major: 0 } })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(3)
|
||||
|
||||
versions = (article.get('version') for article in articles)
|
||||
articles = (article.toObject() for article in articles)
|
||||
|
||||
testArrayEqual(_.pluck(versions, 'major'), [0, 1, 0])
|
||||
testArrayEqual(_.pluck(versions, 'minor'), [0, 0, 1])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMajor'), [false, true, false])
|
||||
testArrayEqual(_.pluck(versions, 'isLatestMinor'), [false, true, true])
|
||||
testArrayEqual(_.pluck(articles, 'name'), ['Article name', 'Article name', 'Article name'])
|
||||
testArrayEqual(_.pluck(articles, 'body'), ['Article body', 'New body', 'New new body'])
|
||||
testArrayEqual(_.pluck(articles, 'slug'), [undefined, 'article-name', undefined])
|
||||
testArrayEqual(_.pluck(articles, 'index'), [undefined, true, undefined])
|
||||
done()
|
||||
|
||||
|
||||
it 'unsets properties which are not included in the request', utils.wrap (done) ->
|
||||
yield postNewVersion({ name: 'Article name', version: { major: 0 } })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(2)
|
||||
expect(articles[1].get('body')).toBeUndefined()
|
||||
done()
|
||||
|
||||
|
||||
it 'works for artisans', utils.wrap (done) ->
|
||||
yield utils.logoutAsync()
|
||||
artisan = yield utils.initArtisanAsync()
|
||||
yield utils.loginUserAsync(artisan)
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' })
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(2)
|
||||
done()
|
||||
|
||||
|
||||
it 'works for normal users submitting translations', utils.wrap (done) ->
|
||||
yield utils.logoutAsync()
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
yield postNewVersion({ name: 'Article name', body: 'Article body', i18n: { fr: { name: 'Le Article' }}}, 201)
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(2)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not work for normal users', utils.wrap (done) ->
|
||||
yield utils.logoutAsync()
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' }, 403)
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(1)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not work for anonymous users', utils.wrap (done) ->
|
||||
yield utils.logoutAsync()
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' }, 401)
|
||||
articles = yield Article.find()
|
||||
expect(articles.length).toBe(1)
|
||||
done()
|
||||
|
||||
|
||||
it 'notifies watchers of changes', utils.wrap (done) ->
|
||||
sendwithus = require '../../../server/sendwithus'
|
||||
spyOn(sendwithus.api, 'send').and.callFake (context, cb) ->
|
||||
expect(context.email_id).toBe(sendwithus.templates.change_made_notify_watcher)
|
||||
expect(context.recipient.address).toBe('test@gmail.com')
|
||||
done()
|
||||
user = yield User({email: 'test@gmail.com', name: 'a user'}).save()
|
||||
article = yield Article.findById(articleID)
|
||||
article.set('watchers', article.get('watchers').concat([user.get('_id')]))
|
||||
yield article.save()
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body', commitMessage: 'Commit message' })
|
||||
|
||||
|
||||
it 'sends a notification to artisan and main HipChat channels', utils.wrap (done) ->
|
||||
hipchat = require '../../../server/hipchat'
|
||||
spyOn(hipchat, 'sendHipChatMessage')
|
||||
yield postNewVersion({ name: 'Article name', body: 'New body' })
|
||||
expect(hipchat.sendHipChatMessage).toHaveBeenCalled()
|
||||
done()
|
||||
|
||||
describe 'version fetching endpoints', ->
|
||||
articleData = { name: 'Original version', body: 'Article body' }
|
||||
articleOriginal = null
|
||||
|
||||
article = {name: 'Yo', body: 'yo ma'}
|
||||
article2 = {name: 'Original', body: 'yo daddy'}
|
||||
postNewVersion = Promise.promisify (json, expectedStatus=201, done) ->
|
||||
if _.isFunction(expectedStatus)
|
||||
done = expectedStatus
|
||||
expectedStatus = 201
|
||||
url = getURL("/db/article/#{articleOriginal}/new-version")
|
||||
request.post { uri: url, json: json }, (err, res) ->
|
||||
expect(res.statusCode).toBe(expectedStatus)
|
||||
done(err)
|
||||
|
||||
url = getURL('/db/article')
|
||||
articles = {}
|
||||
|
||||
it 'does not allow non-admins to create Articles.', (done) ->
|
||||
loginJoe ->
|
||||
request.post {uri: url, json: article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
@admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(@admin)
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
articleOriginal = body._id
|
||||
yield postNewVersion({ name: 'Latest minor version', body: 'New body', version: {major: 0} })
|
||||
yield postNewVersion({ name: 'Latest major version', body: 'New new body' })
|
||||
done()
|
||||
|
||||
it 'allows admins to create Articles', (done) ->
|
||||
loginAdmin ->
|
||||
request.post {uri: url, json: article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.slug).toBeDefined()
|
||||
expect(body.body).toBeDefined()
|
||||
expect(body.name).toBeDefined()
|
||||
expect(body.original).toBeDefined()
|
||||
expect(body.creator).toBeDefined()
|
||||
articles[0] = body
|
||||
|
||||
# Having two articles allow for testing article search and such
|
||||
request.post {uri: url, json: article2}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.slug).toBeDefined()
|
||||
expect(body.body).toBeDefined()
|
||||
expect(body.name).toBeDefined()
|
||||
expect(body.original).toBeDefined()
|
||||
expect(body.creator).toBeDefined()
|
||||
articles[0] = body
|
||||
|
||||
done()
|
||||
|
||||
it 'allows admins to make new minor versions', (done) ->
|
||||
new_article = _.clone(articles[0])
|
||||
new_article.body = 'yo daddy'
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.version.major).toBe(0)
|
||||
expect(body.version.minor).toBe(1)
|
||||
expect(body._id).not.toBe(articles[0]._id)
|
||||
expect(body.parent).toBe(articles[0]._id)
|
||||
expect(body.creator).toBeDefined()
|
||||
articles[1] = body
|
||||
describe 'GET /db/article/:handle/version/:version', ->
|
||||
|
||||
it 'returns the latest version for the given original article when :version is empty', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL("/db/article/#{articleOriginal}/version"), json: true }
|
||||
expect(body.name).toBe('Latest major version')
|
||||
done()
|
||||
|
||||
it 'allows admins to make new major versions', (done) ->
|
||||
new_article = _.clone(articles[1])
|
||||
delete new_article.version
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.version.major).toBe(1)
|
||||
expect(body.version.minor).toBe(0)
|
||||
expect(body._id).not.toBe(articles[1]._id)
|
||||
expect(body.parent).toBe(articles[1]._id)
|
||||
articles[2] = body
|
||||
|
||||
it 'returns the latest of a given major version when :version is X', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL("/db/article/#{articleOriginal}/version/0"), json: true }
|
||||
expect(body.name).toBe('Latest minor version')
|
||||
done()
|
||||
|
||||
it 'grants access for regular users', (done) ->
|
||||
loginJoe ->
|
||||
request.get {uri: url+'/'+articles[0]._id}, (err, res, body) ->
|
||||
body = JSON.parse(body)
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.body).toBe(articles[0].body)
|
||||
done()
|
||||
|
||||
it 'does not allow regular users to make new versions', (done) ->
|
||||
new_article = _.clone(articles[2])
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
|
||||
it 'returns a specific version when :version is X.Y', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL("/db/article/#{articleOriginal}/version/0.0"), json: true }
|
||||
expect(body.name).toBe('Original version')
|
||||
done()
|
||||
|
||||
it 'allows name changes from one version to the next', (done) ->
|
||||
loginAdmin ->
|
||||
new_article = _.clone(articles[0])
|
||||
new_article.name = 'Yo mama now is the larger'
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.name).toBe(new_article.name)
|
||||
done()
|
||||
|
||||
it 'get schema', (done) ->
|
||||
request.get {uri: url+'/schema'}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
body = JSON.parse(body)
|
||||
expect(body.type).toBeDefined()
|
||||
|
||||
it 'returns 422 when the original value is invalid', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL('/db/article/dne/version'), json: true }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
it 'returns 404 when the original value cannot be found', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL('/db/article/012345678901234567890123/version'), json: true }
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/article/:handle/versions', ->
|
||||
|
||||
it 'returns an array of versions sorted by creation for the given original article', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL("/db/article/#{articleOriginal}/versions"), json: true }
|
||||
expect(body.length).toBe(3)
|
||||
expect(body[0].name).toBe('Latest major version')
|
||||
expect(body[1].name).toBe('Latest minor version')
|
||||
expect(body[2].name).toBe('Original version')
|
||||
done()
|
||||
|
||||
it 'projects most properties by default', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL("/db/article/#{articleOriginal}/versions"), json: true }
|
||||
expect(body[0].body).toBeUndefined()
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/article/:handle/files', ->
|
||||
|
||||
it 'returns an array of file metadata for the given original article', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
articleData = { name: 'Article', body: 'Article' }
|
||||
admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(admin)
|
||||
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
[res, body] = yield request.postAsync(getURL('/file'), { json: {
|
||||
url: getURL('/assets/main.html')
|
||||
filename: 'test.html'
|
||||
path: 'db/article/'+article.original
|
||||
mimetype: 'text/html'
|
||||
}})
|
||||
[res, body] = yield request.getAsync(getURL('/db/article/'+article.original+'/files'), {json: true})
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].filename).toBe('test.html')
|
||||
expect(body[0].metadata.path).toBe('db/article/'+article.original)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET and POST /db/article/:handle/names', ->
|
||||
articleData1 = { name: 'Article 1', body: 'Article 1 body' }
|
||||
articleData2 = { name: 'Article 2', body: 'Article 2 body' }
|
||||
|
||||
it 'does not allow naming an article a reserved word', (done) ->
|
||||
loginAdmin ->
|
||||
new_article = {name: 'Names', body: 'is a reserved word'}
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
it 'returns an object mapping ids to names', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(admin)
|
||||
[res, article1] = yield request.postAsync(getURL('/db/article'), { json: articleData1 })
|
||||
[res, article2] = yield request.postAsync(getURL('/db/article'), { json: articleData2 })
|
||||
yield utils.logoutAsync()
|
||||
[res, body] = yield request.getAsync { uri: getURL('/db/article/names?ids='+[article1._id, article2._id].join(',')), json: true }
|
||||
expect(body.length).toBe(2)
|
||||
expect(body[0].name).toBe('Article 1')
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/article/names?ids='+[article1._id, article2._id].join(',')), json: true }
|
||||
expect(body.length).toBe(2)
|
||||
expect(body[0].name).toBe('Article 1')
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/article/:handle/patches', ->
|
||||
|
||||
it 'returns pending patches for the given original article', utils.wrap (done) ->
|
||||
yield utils.clearModelsAsync([Article])
|
||||
articleData = { name: 'Article', body: 'Article' }
|
||||
admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(admin)
|
||||
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
[res, patch] = yield request.postAsync { uri: getURL('/db/patch'), json: {
|
||||
delta: []
|
||||
commitMessage: 'Test commit'
|
||||
target: {
|
||||
collection: 'article'
|
||||
id: article._id
|
||||
}
|
||||
}}
|
||||
[res, patches] = yield request.getAsync getURL("/db/article/#{article._id}/patches"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(patches.length).toBe(1)
|
||||
expect(patches[0]._id).toBe(patch._id)
|
||||
done()
|
||||
|
||||
it 'returns 422 for invalid object ids', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL("/db/article/invalid/patches"), { json: true }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/article/:handle/watchers', ->
|
||||
|
||||
it 'adds self to the list of watchers, and is idempotent', utils.wrap (done) ->
|
||||
# create article
|
||||
yield utils.clearModelsAsync([Article])
|
||||
articleData = { name: 'Article', body: 'Article' }
|
||||
admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(admin)
|
||||
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
|
||||
# add new user as watcher
|
||||
yield utils.logoutAsync()
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
[res, article] = yield request.postAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(_.contains(article.watchers, user.id)).toBe(true)
|
||||
|
||||
it 'allows regular users to get all articles', (done) ->
|
||||
loginJoe ->
|
||||
request.get {uri: url, json: {}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
||||
# check idempotence, db
|
||||
numWatchers = article.watchers.length
|
||||
[res, article] = yield request.postAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(numWatchers).toBe(article.watchers.length)
|
||||
article = yield Article.findById(article._id)
|
||||
expect(_.last(article.get('watchers')).toString()).toBe(user.id)
|
||||
done()
|
||||
|
||||
|
||||
it 'allows regular users to get articles and use projection', (done) ->
|
||||
loginJoe ->
|
||||
# default projection
|
||||
request.get {uri: url + '?project=true', json: {}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
expect(body[0].body).toBeUndefined()
|
||||
expect(body[0].version).toBeDefined()
|
||||
describe 'DELETE /db/article/:handle/watchers', ->
|
||||
|
||||
it 'removes self from the list of watchers, and is idempotent', utils.wrap (done) ->
|
||||
# create article
|
||||
yield utils.clearModelsAsync([Article])
|
||||
articleData = { name: 'Article', body: 'Article' }
|
||||
admin = yield utils.initAdminAsync({})
|
||||
yield utils.loginUserAsync(admin)
|
||||
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
|
||||
# custom projection
|
||||
request.get {uri: url + '?project=original', json: {}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
expect(Object.keys(body[0]).length).toBe(2)
|
||||
expect(body[0].original).toBeDefined()
|
||||
done()
|
||||
# add new user as watcher
|
||||
yield utils.logoutAsync()
|
||||
user = yield utils.initUserAsync()
|
||||
yield utils.loginUserAsync(user)
|
||||
[res, article] = yield request.postAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
|
||||
expect(_.contains(article.watchers, user.id)).toBe(true)
|
||||
|
||||
it 'allows regular users to perform a text search', (done) ->
|
||||
loginJoe ->
|
||||
request.get {uri: url + '?term="daddy"', json: {}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].name).toBe(article2.name)
|
||||
expect(body[0].body).toBe(article2.body)
|
||||
done()
|
||||
# remove user as watcher
|
||||
[res, article] = yield request.delAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(_.contains(article.watchers, user.id)).toBe(false)
|
||||
|
||||
# check idempotence, db
|
||||
numWatchers = article.watchers.length
|
||||
[res, article] = yield request.delAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(numWatchers).toBe(article.watchers.length)
|
||||
article = yield Article.findById(article._id)
|
||||
ids = (id.toString() for id in article.get('watchers'))
|
||||
expect(_.contains(ids, user.id)).toBe(false)
|
||||
done()
|
|
@ -14,17 +14,18 @@ describe '/db/<id>/version', ->
|
|||
it 'sets up', (done) ->
|
||||
loginAdmin ->
|
||||
request.post {uri: url, json: article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.statusCode).toBe(201)
|
||||
articles[0] = body
|
||||
new_article = _.clone(articles[0])
|
||||
new_article.body = '...'
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
newVersionURL = "#{url}/#{new_article._id}/new-version"
|
||||
request.post {uri: newVersionURL, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(201)
|
||||
articles[1] = body
|
||||
new_article = _.clone(articles[1])
|
||||
delete new_article.version
|
||||
request.post {uri: url, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
request.post {uri: newVersionURL, json: new_article}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(201)
|
||||
articles[2] = body
|
||||
done()
|
||||
|
||||
|
|
|
@ -385,18 +385,19 @@ describe 'Statistics', ->
|
|||
expect(carl.get User.statsMapping.edits.article).toBeUndefined()
|
||||
article.creator = carl.get 'id'
|
||||
|
||||
# Create major version 1.0
|
||||
# Create major version 0.0
|
||||
request.post {uri:url, json: article}, (err, res, body) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toBe 200
|
||||
expect(res.statusCode).toBe 201
|
||||
article = body
|
||||
|
||||
User.findById carl.get('id'), (err, guy) ->
|
||||
expect(err).toBeNull()
|
||||
expect(guy.get User.statsMapping.edits.article).toBe 1
|
||||
|
||||
# Create minor version 1.1
|
||||
request.post {uri:url, json: article}, (err, res, body) ->
|
||||
# Create minor version 0.1
|
||||
newVersionURL = "#{url}/#{article._id}/new-version"
|
||||
request.post {uri:newVersionURL, json: article}, (err, res, body) ->
|
||||
expect(err).toBeNull()
|
||||
|
||||
User.findById carl.get('id'), (err, guy) ->
|
||||
|
|
67
spec/server/utils.coffee
Normal file
67
spec/server/utils.coffee
Normal file
|
@ -0,0 +1,67 @@
|
|||
async = require 'async'
|
||||
utils = require '../../server/lib/utils'
|
||||
co = require 'co'
|
||||
|
||||
module.exports = mw =
|
||||
getURL: (path) -> 'http://localhost:3001' + path
|
||||
|
||||
clearModels: (models, done) ->
|
||||
funcs = []
|
||||
for model in models
|
||||
wrapped = (m) ->
|
||||
(callback) ->
|
||||
m.remove {}, (err) ->
|
||||
callback(err, true)
|
||||
funcs.push(wrapped(model))
|
||||
async.parallel funcs, done
|
||||
|
||||
initUser: (options, done) ->
|
||||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
options = _.extend({permissions: []}, options)
|
||||
doc = {
|
||||
email: 'user'+_.uniqueId()+'@gmail.com'
|
||||
password: 'password'
|
||||
permissions: options.permissions
|
||||
}
|
||||
new User(doc).save (err, user) ->
|
||||
expect(err).toBe(null)
|
||||
done(err, user)
|
||||
|
||||
loginUser: (user, done) ->
|
||||
form = {
|
||||
username: user.get('email')
|
||||
password: 'password'
|
||||
}
|
||||
request.post mw.getURL('/auth/login'), { form: form }, (err, res) ->
|
||||
expect(err).toBe(null)
|
||||
expect(res.statusCode).toBe(200)
|
||||
done(err, user)
|
||||
|
||||
initAdmin: (options, done) ->
|
||||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
options = _.extend({permissions: ['admin']}, options)
|
||||
return @initUser(options, done)
|
||||
|
||||
initArtisan: (options, done) ->
|
||||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
options = _.extend({permissions: ['artisan']}, options)
|
||||
return @initUser(options, done)
|
||||
|
||||
logout: (done) ->
|
||||
request.post mw.getURL('/auth/logout'), done
|
||||
|
||||
wrap: (gen) ->
|
||||
fn = co.wrap(gen)
|
||||
return (done) ->
|
||||
fn.apply(@, [done]).catch (err) -> done.fail(err)
|
||||
|
||||
|
||||
|
||||
Promise = require 'bluebird'
|
||||
Promise.promisifyAll(module.exports)
|
Loading…
Reference in a new issue