diff --git a/.travis.yml b/.travis.yml index 83caf4b..0bfc265 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js -services: mongodb +services: + - mongodb + - elasticsearch before_install: - npm install -g grunt-cli diff --git a/app.json b/app.json index 8018ea7..3cd9314 100644 --- a/app.json +++ b/app.json @@ -15,5 +15,5 @@ "BUILDPACK_URL": "https://github.com/mbuchetics/heroku-buildpack-nodejs-grunt.git", "NODE_ENV": "production" }, - "addons": ["mongohq", "mandrill"] + "addons": ["mongohq", "mandrill", "searchbox"] } diff --git a/bower.json b/bower.json index 05b01cf..1b8ada8 100644 --- a/bower.json +++ b/bower.json @@ -27,7 +27,8 @@ "fastclick": "~1.0.2", "gsap": "~1.13.1", "seedrandom": "~2.3.6", - "Sortable": "~0.3.0" + "Sortable": "~0.3.0", + "typeahead.js": "~0.10.5" }, "exportsOverride": { "jquery.formParams": { diff --git a/client/source/controllers/buckets_controller.coffee b/client/source/controllers/buckets_controller.coffee index cf6d091..46b6a2f 100644 --- a/client/source/controllers/buckets_controller.coffee +++ b/client/source/controllers/buckets_controller.coffee @@ -55,7 +55,7 @@ module.exports = class BucketsController extends Controller compose: (options) -> @entries = new Entries - @entries.fetch( data: {bucket: bucket.get('slug'), status: ''}, processData: yes ).done => + @entries.fetchWithParams( bucket: bucket.get('slug'), status: '' ).done => @view = new EntriesBrowser collection: @entries bucket: bucket diff --git a/client/source/helpers.coffee b/client/source/helpers.coffee index e4bea9f..02b0003 100644 --- a/client/source/helpers.coffee +++ b/client/source/helpers.coffee @@ -37,7 +37,7 @@ Handlebars.registerHelper 'timeAgo', (dateTime) -> expanded = Handlebars.helpers.simpleDateTime dateTime new Handlebars.SafeString """ - + #{moment(dateTime).fromNow()} """ diff --git a/client/source/models/entries.coffee b/client/source/models/entries.coffee index 39e0e42..9c86a79 100644 --- a/client/source/models/entries.coffee +++ b/client/source/models/entries.coffee @@ -9,5 +9,8 @@ module.exports = class Entries extends Collection @url += "?#{$.param(bucketId: bucketId)}" @fetch() + fetchWithParams: (params={}) -> + @fetch({data: params}, processData: yes) + comparator: (entry) -> - new Date(entry.get('publishDate')).getTime() diff --git a/client/source/templates/buckets/dashboard.hbs b/client/source/templates/buckets/dashboard.hbs index 8dadaf4..af27796 100644 --- a/client/source/templates/buckets/dashboard.hbs +++ b/client/source/templates/buckets/dashboard.hbs @@ -3,5 +3,8 @@

Buckets is an open-source CMS, built in Node.js, which is being actively developed by the community at Assembly.

+ + +
diff --git a/client/source/templates/entries/suggestion.hbs b/client/source/templates/entries/suggestion.hbs new file mode 100644 index 0000000..04ddc09 --- /dev/null +++ b/client/source/templates/entries/suggestion.hbs @@ -0,0 +1 @@ +{{icon bucket.icon}} {{title}}
{{timeAgo publishDate}} diff --git a/client/source/views/buckets/dashboard.coffee b/client/source/views/buckets/dashboard.coffee index 650b3ad..62a7ed8 100644 --- a/client/source/views/buckets/dashboard.coffee +++ b/client/source/views/buckets/dashboard.coffee @@ -1,6 +1,48 @@ +_ = require 'underscore' +Chaplin = require 'chaplin' PageView = require 'views/base/page' +mediator = require 'mediator' + +suggestionTpl = require 'templates/entries/suggestion' tpl = require 'templates/buckets/dashboard' module.exports = class DashboardView extends PageView template: tpl + + events: + 'typeahead:selected': 'selectItem' + + render: -> + super + + engine = new Bloodhound + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value') + queryTokenizer: Bloodhound.tokenizers.whitespace + prefetch: '/api/entries' + remote: '/api/entries/?query=%QUERY*' + + engine.initialize() + engine.clearRemoteCache(); + + @$search = @$('[name="search"]').typeahead + highlight: yes + , + name: 'entries', + displayKey: 'title' + templates: { + empty: """ +
+ No matching entries. +
+ """ + suggestion: suggestionTpl + } + source: engine.ttAdapter() + + _.defer => @$search.focus() + + selectItem: (e, item) -> + Chaplin.utils.redirectTo 'buckets#browse', + slug: item.bucket.slug + entryID: item.id diff --git a/client/style/index.styl b/client/style/index.styl index ef542b1..1e3b7bc 100644 --- a/client/style/index.styl +++ b/client/style/index.styl @@ -18,10 +18,9 @@ $sans = 'Avenir Next', Avenir, 'Helvetica Neue', Helvetica, 'Segoe UI', Segoe, R body background-color #fcfcfc - display table width 100% + overflow-x hidden display flex - min-height 100vh flex-direction column #bkts-content @@ -157,3 +156,45 @@ input[type="text"].form-control, textarea.form-control .pageBreadcrumb margin 10px 0 background-color #f1f1f1 + + +.tt-query + width 300px + outline none + +.tt-hint + color: #999 + +.tt-highlight + background-color rgba(lighten($yellow, 30), .5) + +.tt-dropdown-menu + margin-top 12px + padding 2px 0 + background-color #fff + border 1px solid #ccc + border 1px solid rgba(0, 0, 0, 0.2) + border-radius 2px + width 100% + color #555 + + .icon + vertical-align middle + display inline-block + font-size 24px + +.tt-suggestion + padding: 3px 20px + font-size: 18px + line-height: 24px + +.tt-suggestion.tt-cursor + color #222 + background-color lighten($primary, 30) + cursor pointer + +.tt-suggestion p + margin: 0 + +.tt-empty + padding: 3px 20px diff --git a/migrations/20140810052927866_sync_with_es.coffee b/migrations/20140810052927866_sync_with_es.coffee new file mode 100644 index 0000000..9c8c671 --- /dev/null +++ b/migrations/20140810052927866_sync_with_es.coffee @@ -0,0 +1,27 @@ +Bucket = require '../server/models/bucket' +Entry = require '../server/models/entry' + +http = require 'http' +config = require '../server/config' + +module.exports = + requiresDowntime: no # true or false + + up: (done) -> + Entry.update {content: null}, {content: {}}, {multi: yes}, (e, entries) -> + throw e if e + + Entry.createMapping (err) -> + throw e if e + stream = Entry.synchronize() + count = 0 + + stream.on 'data', (e, doc) -> count++ + stream.on 'close', -> + console.log('Indexed ' + count + ' entries into ElasticSearch') + done() + + stream.on 'error', (e) -> throw e + + down: (done) -> + done() diff --git a/package.json b/package.json index acccbe4..74a9511 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "author": "David Kaneda (http://www.davidkaneda.com)", "license": "AGPL-3.0", "bugs": { - "url": "https://assemblymade.com/buckets/wips" + "url": "https://assembly.com/buckets/wips" }, "keywords": [ "cms", @@ -78,6 +78,7 @@ "inflection": "^1.3.6", "marked": "^0.3.2", "moment": "^2.7.0", + "mongoosastic": "^0.4.0", "mongoose": "^3.8.15", "mongoose-regexp": "0.0.1", "mongoose-unique-validator": "^0.3.0", diff --git a/server/config.coffee b/server/config.coffee index 881b933..9b9422e 100644 --- a/server/config.coffee +++ b/server/config.coffee @@ -14,6 +14,9 @@ config = catchAll: yes autoStart: yes db: "mongodb://localhost/buckets_#{env}" + elasticsearch: + url: process.env.SEARCHBOX_SSL_URL || 'http://127.0.0.1:9200' + index: "buckets_entries_#{env}" production: smtp: service: 'Mandrill' diff --git a/server/models/bucket.coffee b/server/models/bucket.coffee index 5e5c65b..b3a9a4e 100644 --- a/server/models/bucket.coffee +++ b/server/models/bucket.coffee @@ -8,7 +8,6 @@ db = require '../lib/database' fieldSchema = new mongoose.Schema name: type: String - unique: no required: yes slug: type: String @@ -21,8 +20,7 @@ fieldSchema = new mongoose.Schema required: yes settings: mongoose.Schema.Types.Mixed -fieldSchema - .path 'slug' +fieldSchema.path 'slug' .validate (val) -> val not in [ 'title' @@ -58,12 +56,30 @@ bucketSchema = new mongoose.Schema required: yes icon: type: String - enum: ['edit', 'photos', 'calendar', 'movie', 'music-note', 'map-pin', 'quote', 'artboard', 'contacts-1'] + enum: [ + 'edit' + 'photos' + 'calendar' + 'movie' + 'music-note' + 'map-pin' + 'quote' + 'artboard' + 'contacts-1' + ] default: 'edit' required: yes color: type: String - enum: ['teal', 'purple', 'red', 'yellow', 'blue', 'orange', 'green'] + enum: [ + 'teal' + 'purple' + 'red' + 'yellow' + 'blue' + 'orange' + 'green' + ] default: 'teal' required: yes urlPattern: String @@ -95,7 +111,7 @@ bucketSchema.path('urlPattern').validate (value) -> bucketSchema.plugin uniqueValidator, message: '“{VALUE}” is already taken.' bucketSchema.post 'remove', -> - @model('Entry').remove(bucket: @_id).exec() + @model('Entry').find(bucket: @_id).remove().exec() bucketSchema.methods.getMembers = (callback) -> @model('User').find diff --git a/server/models/entry.coffee b/server/models/entry.coffee index d13f58f..a1c07cf 100755 --- a/server/models/entry.coffee +++ b/server/models/entry.coffee @@ -1,9 +1,12 @@ mongoose = require 'mongoose' +mongoosastic = require 'mongoosastic' _ = require 'underscore' chrono = require 'chrono-node' async = require 'async' getSlug = require 'speakingurl' +url = require 'url' +config = require '../config' db = require '../lib/database' # Add a parser to Chrono to understand "now" @@ -36,7 +39,10 @@ entrySchema = new mongoose.Schema title: type: String required: yes - description: String + es_boost: 3.0 + description: + type: String + es_boost: 2.0 slug: type: String status: @@ -46,11 +52,15 @@ entrySchema = new mongoose.Schema default: 'live' lastModified: type: Date + es_type: 'date' + default: Date.now publishDate: type: Date + es_type: 'date' default: Date.now createdDate: type: Date + es_type: 'date' default: Date.now author: type: mongoose.Schema.Types.ObjectId @@ -60,10 +70,16 @@ entrySchema = new mongoose.Schema type: mongoose.Schema.Types.ObjectId ref: 'Bucket' required: yes - keywords: [ - type: String - ] - content: {} + keywords: + type: [String] + default: [] + es_boost: 2.0 + es_type: 'string' + content: + type: {} + default: {} + es_type : 'object' + es_boost: 2.0 , toJSON: virtuals: yes @@ -79,13 +95,15 @@ entrySchema.pre 'save', (next) -> entrySchema.pre 'validate', (next) -> @slug ?= getSlug @title - @model('Bucket').findOne _id: @bucket, (err, bkt) => + mongoose.model('Bucket').findOne _id: @bucket, (err, bkt) => @invalidate 'bucket', 'Must belong to a bucket' unless bkt for field in bkt?.fields or [] - if field.settings?.required and !@content[field.slug] + if field.settings?.required and !@content[field.slug]? @invalidate field.slug, 'required' + else if @content[field.slug] is '' + delete @content[field.slug] next() @@ -112,41 +130,74 @@ entrySchema.statics.findByParams = (params, callback) -> sort: '-publishDate' find: '' slug: null + search: null + query: null searchQuery = {} async.parallel [ (callback) -> - if settings.bucket? - filteredBuckets = settings.bucket.split '|' - searchQuery.bucket = $in: [] + if settings.bucket?.length > 0 + bucketStrings = settings.bucket.split '|' + + return callback null if bucketStrings.length is 0 - mongoose.model('Bucket').find {slug: $in: filteredBuckets}, (err, buckets) => - filteredBucketIDs = _.pluck _.filter(buckets, (bkt) -> bkt.slug in filteredBuckets), '_id' + isNegated = (bkt) -> bkt.substr(0,1) is '-' + + rejectedBuckets = _.map _.filter(bucketStrings, isNegated), (bkt) -> + bkt.substr(1) + filteredBuckets = _.reject bucketStrings, isNegated + + $findParams = if rejectedBuckets.length + slug: $nin: rejectedBuckets + else + slug: $in: filteredBuckets + + mongoose.model('Bucket').find $findParams, (err, buckets) => + filteredBucketIDs = _.pluck buckets, '_id' searchQuery.bucket = $in: filteredBucketIDs callback null else callback null + ], => if settings.slug searchQuery.slug = settings.slug - if settings.where - searchQuery.$where = settings.where - if settings.status?.length > 0 searchQuery.status = settings.status + if settings.search? + # Some default manipulation to make the search fuzzy + # and case insensitive by default + return @search query: + fuzzy: + _all: + value: "#{settings.search.toLowerCase()}*" + fuzziness: 5 + prefix_length: 2 + , (err, elasticEntries) -> + throw err if err + callback null, elasticEntries.hits + + if settings.query? + return @search query: + query_string: + query: settings.query + + , (err, elasticEntries) -> + if err + callback err, [] + else if elasticEntries.hits + callback null, elasticEntries.hits + if settings.since or settings.until searchQuery.publishDate = {} searchQuery.publishDate.$gt = new Date(chrono.parseDate settings.since) if settings.since searchQuery.publishDate.$lte = new Date(chrono.parseDate settings.until) if settings.until @find searchQuery - .populate 'bucket' - .populate - path: 'author' - select: '-passwordDigest -resetPasswordToken -resetPasswordExpires' + .populate 'bucket author' .sort settings.sort .limit settings.limit .skip settings.skip @@ -156,4 +207,15 @@ entrySchema.statics.findByParams = (params, callback) -> else callback null, entries +# Add Elasticsearch via mongoosastic +elasticConnection = url.parse config.elasticsearch.url +entrySchema.plugin mongoosastic, + index: config.elasticsearch.index + host: elasticConnection.hostname + auth: elasticConnection.auth + port: elasticConnection.port + hydrate: yes + hydrateOptions: + populate: 'bucket author' + module.exports = db.model 'Entry', entrySchema diff --git a/server/routes/api/buckets.coffee b/server/routes/api/buckets.coffee index 8115307..1e037c6 100644 --- a/server/routes/api/buckets.coffee +++ b/server/routes/api/buckets.coffee @@ -151,6 +151,7 @@ module.exports = app = express() app.route('/buckets') .post (req, res) -> + return res.status(401).end() unless req.user newBucket = new Bucket req.body diff --git a/server/routes/api/install.coffee b/server/routes/api/install.coffee index 11bac4c..6cd8f8f 100644 --- a/server/routes/api/install.coffee +++ b/server/routes/api/install.coffee @@ -31,8 +31,8 @@ app.post '/install', (req, res) -> Bucket.create bucketSeed ).then( (bucket) -> for entry in entrySeed - entry.bucket = bucket.id - entry.author = newUser.id + entry.bucket = bucket._id + entry.author = newUser._id Entry.create entrySeed , renderError).then -> req.login newUser, -> diff --git a/server/routes/api/routes.coffee b/server/routes/api/routes.coffee index 17b571e..eceb930 100644 --- a/server/routes/api/routes.coffee +++ b/server/routes/api/routes.coffee @@ -51,9 +51,9 @@ app.route('/routes') Route.find().sort(urlPattern: 1).exec (err, routes) -> if err - res.send err, 400 + res.status(400).send err else if routes - res.send routes + res.status(200).send routes .post (req, res) -> ### @@ -70,9 +70,9 @@ app.route('/routes') newRoute.save (err) -> if err - res.send 400, err + res.status(400).send err else - res.send 201, newRoute + res.status(201).send newRoute ### @api {post} /routes/:id Remove a Route diff --git a/test/reset.coffee b/test/reset.coffee index a7ad598..9268126 100644 --- a/test/reset.coffee +++ b/test/reset.coffee @@ -1,12 +1,20 @@ +async = require 'async' db = require '../server/lib/database' +mongoose = require 'mongoose' config = require '../server/config' buckets = require('../server') Entry = require '../server/models/entry' +MongoosasticConfig = require 'mongoosastic/test/config' + dropDatabase = (done) -> prep -> db.connection.db.dropDatabase done +dropIndex = (done) -> + MongoosasticConfig.deleteIndexIfExists [config.elasticsearch.index], -> + Entry.createMapping done + prep = (done) -> if db.connection.readyState is 1 done() @@ -22,4 +30,4 @@ module.exports = prep: prep server: server all: (done) -> - dropDatabase done + async.parallel [dropIndex, dropDatabase], done diff --git a/test/server/models/activity.coffee b/test/server/models/activity.coffee index ef27015..ea1a4e1 100644 --- a/test/server/models/activity.coffee +++ b/test/server/models/activity.coffee @@ -1,17 +1,14 @@ -db = require '../../../server/lib/database' mongoose = require 'mongoose' Activity = require '../../../server/models/activity' +reset = require '../../reset' {expect} = require 'chai' sinon = require 'sinon' describe 'Activity', -> - - afterEach (done) -> - for _, c of db.connection.collections - c.remove(->) - done() + before reset.prep + afterEach reset.db describe 'Validation', -> it 'requires an actor', (done) -> diff --git a/test/server/models/bucket.coffee b/test/server/models/bucket.coffee index dfda847..77ebb37 100644 --- a/test/server/models/bucket.coffee +++ b/test/server/models/bucket.coffee @@ -1,37 +1,33 @@ User = require('../../../server/models/user') Bucket = require('../../../server/models/bucket') -db = require('../../../server/lib/database') reset = require '../../reset' -{assert} = require('chai') +{assert} = require 'chai' describe 'Bucket', -> - before reset.db - after reset.db + afterEach reset.db describe '#getMembers', -> - u = null - b = null + bucket = null + user = null before (done) -> - Bucket.create { name: 'Images', slug: 'images' }, (e, bucket) -> + Bucket.create { name: 'Images', slug: 'images' }, (e, bkt) -> throw e if e - u = new User - name: 'Bucketer' - email: 'hello@buckets.io' - password: 'S3cr3ts' - b = bucket - done() + bucket = bkt + User.create { name: 'Bucketer', email: 'hello@buckets.io', password: 'S3cr3ts' }, (e, u) -> + user = u + u.upsertRole 'contributor', bucket, (e) -> + throw e if e + done() it 'returns members', (done) -> + bucket.getMembers (e, users) -> + throw e if e + assert.isArray(users) + assert.lengthOf(users, 1) + assert.equal(users[0].id, user.id) - u.upsertRole 'contributor', b, (e, user) -> - throw e if e - b.getMembers (e, users) -> - assert.isArray(users) - assert.lengthOf(users, 1) - assert.equal(users[0].id, u.id) - - done() + done() diff --git a/test/server/models/entry.coffee b/test/server/models/entry.coffee index 08a3eea..1ed1d3e 100644 --- a/test/server/models/entry.coffee +++ b/test/server/models/entry.coffee @@ -1,4 +1,5 @@ -db = require '../../../server/lib/database' +config = require '../../../server/config' +reset = require '../../reset' Entry = require '../../../server/models/entry' Bucket = require '../../../server/models/bucket' @@ -8,6 +9,8 @@ reset = require '../../reset' {expect} = require 'chai' +MongoosasticConfig = require 'mongoosastic/test/config' + describe 'Entry', -> user = null @@ -22,12 +25,7 @@ describe 'Entry', -> user = u done() - beforeEach (done) -> - for _, c of db.connection.collections - c.remove(->) - done() - - afterEach reset.db + after reset.all describe 'Validation', -> @@ -49,10 +47,17 @@ describe 'Entry', -> bucketId = null beforeEach (done) -> - Bucket.create {name: 'Articles', slug: 'articles'}, (e, bucket) -> + # Create a Bucket to put our Test Entries in + Bucket.create + name: 'Articles' + slug: 'articles' + , (e, bucket) -> + throw e if e bucketId = bucket._id done() + afterEach reset.db + it 'parses dates from strings', (done) -> Entry.create title: 'New Entry' @@ -61,7 +66,7 @@ describe 'Entry', -> author: user._id , (e, entry) -> expected = new Date - expected.setHours(21, 0, 0, 0) + expected.setHours 21, 0, 0, 0 # 9pm expect(expected.toISOString()).equal(entry.publishDate.toISOString()) done() @@ -71,9 +76,20 @@ describe 'Entry', -> expect(entry.slug).to.equal 'resumes-and-cvs' done() + it 'automatically sets the publishDate', (done) -> + Entry.create + title: 'New Entry' + bucket: bucketId + author: user._id + , (e, entry) -> + expect(entry.publishDate).to.be.a 'Date' + done() + describe '#findByParams', -> + @timeout 4000 # We do a network call to reset the ES index + # Set up a bunch of entries to filter/search - beforeEach (done) -> + before (done) -> reset.all -> Bucket.create [ name: 'Articles' slug: 'articles' @@ -81,6 +97,8 @@ describe 'Entry', -> name: 'Photos' slug: 'photos' ], (e, articleBucket, photoBucket) -> + throw e if e + Entry.create [ title: 'Test Article' bucket: articleBucket._id @@ -88,22 +106,36 @@ describe 'Entry', -> status: 'live' publishDate: '2 days ago' , - title: 'Test Photo' + title: 'Test Photoset' bucket: photoBucket._id author: user._id status: 'live' - ], -> - done() + ], done + + after reset.db it 'filters by bucket slug (empty)', (done) -> Entry.findByParams bucket: '', (e, entries) -> - expect(entries).to.have.length 0 + expect(entries).to.have.length 2 done() it 'filters by bucket slug', (done) -> Entry.findByParams bucket: 'photos', (e, entries) -> + throw e if e expect(entries).to.have.length 1 - expect(entries?[0]?.title).to.equal 'Test Photo' + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'filters by bucket slug (invalid)', (done) -> + Entry.findByParams bucket: 'asdf', (e, entries) -> + throw e if e + expect(entries).to.have.length 0 + done() + + it 'filters by bucket slug (w/negation)', (done) -> + Entry.findByParams bucket: '-articles', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' done() it 'filters by multiple bucket slugs', (done) -> @@ -114,7 +146,7 @@ describe 'Entry', -> it 'filters with `since`', (done) -> Entry.findByParams since: 'yesterday', (e, entries) -> expect(entries).to.have.length 1 - expect(entries?[0]?.title).to.equal 'Test Photo' + expect(entries?[0]?.title).to.equal 'Test Photoset' done() it 'filters with `until`', (done) -> @@ -122,3 +154,75 @@ describe 'Entry', -> expect(entries).to.have.length 1 expect(entries?[0]?.title).to.equal 'Test Article' done() + + describe 'Search', -> + before (done) -> + @timeout 5000 + + reset.all -> + Bucket.create [ + name: 'Articles' + slug: 'articles' + , + name: 'Photos' + slug: 'photos' + ], (e, articleBucket, photoBucket) -> + throw e if e + + Entry.create [ + title: 'Test Article' + bucket: articleBucket._id + author: user._id + status: 'live' + publishDate: '2 days ago' + , + title: 'Test Photoset' + bucket: photoBucket._id + author: user._id + status: 'live' + keywords: ['summer'] + ], -> + # This is super painful, but only way I can + # think to test a live elasticsearch instance + Entry.synchronize -> Entry.refresh -> setTimeout done, 2000 + + it 'performs a fuzzy search with `search`', (done) -> + Entry.findByParams search: 'photoste', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'can negate a term', (done) -> + Entry.findByParams query: '-article', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'can check for specific attribute', (done) -> + Entry.findByParams query: '_exists_:keywords', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'can search for specific attribute', (done) -> + Entry.findByParams query: 'keywords:summ*', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'doesn’t throw an exception for bad query', (done) -> + Entry.findByParams query: 'nokey:(6', (e, entries) -> + expect(entries).to.have.length 0 + done() + + it 'searches keywords', (done) -> + Entry.findByParams query: 'summer', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() + + it 'searches phrases', (done) -> + Entry.findByParams query: '"Test Photoset"', (e, entries) -> + expect(entries).to.have.length 1 + expect(entries?[0]?.title).to.equal 'Test Photoset' + done() diff --git a/test/server/models/user.coffee b/test/server/models/user.coffee index b93c33d..6af6d5b 100644 --- a/test/server/models/user.coffee +++ b/test/server/models/user.coffee @@ -73,15 +73,15 @@ describe 'User', -> assert.equal(user.roles[0].name, 'editor') done() - describe 'Updates existing roles', -> - beforeEach (done) -> - user.upsertRole('editor', bucket, done) - - it 'updates the role for a given resource', (done) -> - user.upsertRole 'contributor', bucket, (e, u) -> - assert.lengthOf(user.roles, 1) - assert.equal(user.roles[0].name, 'contributor') - done() + describe 'Updates existing roles', -> + beforeEach (done) -> + user.upsertRole('editor', bucket, done) + + it 'updates the role for a given resource', (done) -> + user.upsertRole 'contributor', bucket, (e, u) -> + assert.lengthOf(user.roles, 1) + assert.equal(user.roles[0].name, 'contributor') + done() describe '#removeRole', -> beforeEach (done) -> diff --git a/test/server/routes/api/buckets.coffee b/test/server/routes/api/buckets.coffee index 94eb459..b9d3928 100644 --- a/test/server/routes/api/buckets.coffee +++ b/test/server/routes/api/buckets.coffee @@ -1,19 +1,57 @@ +path = require 'path' request = require 'supertest' -User = require '../../../../server/models/user' -Bucket = require '../../../../server/models/bucket' + +serverPath = path.resolve __dirname, '../../../../server' +User = require "#{serverPath}/models/user" +Bucket = require "#{serverPath}/models/bucket" reset = require '../../../reset' -{assert} = require 'chai' +{expect} = require 'chai' describe 'Buckets routes', -> app = null - before (done) -> reset.db -> - app = reset.server done - afterEach (done) -> - db.connection.db.dropDatabase done + before (done) -> + app = reset.server -> + Bucket.create [ + name: 'Photos' + slug: 'photos' + , + name: 'Docs' + slug: 'docs' + ], done + + after reset.db + + describe 'GET /buckets', -> + it 'returns a 401 if unauthorized', (done) -> + request(app) + .get('/api/buckets') + .expect(401) + .end (e, res) -> + throw e if e + expect(res.body).to.be.an 'Object' + done() + + it 'returns 200 with buckets' + + describe 'POST /buckets', -> + it 'returns a 401 if not logged in', (done) -> + request(app) + .post('/api/buckets') + .expect(401) + .end (e, res) -> + throw e if e + expect(res.body).to.be.an 'Object' + done() + + it 'returns a 401 if not an admin' + it 'returns a 200 if bucket is created' describe 'GET /buckets/:bucket/members', -> + + # STUB: Need to add auth + #it 'returns the members of a given bucket', (done) -> #Bucket.create { name: 'Products', slug: 'products', singular: 'product' }, (e, bucket) -> #u = new User({ name: 'Bucketer', email: 'hello@buckets.io', password: 'S3cr3ts' }) diff --git a/user/templates/index.hbs b/user/templates/index.hbs index 4311d69..a3a4b06 100644 --- a/user/templates/index.hbs +++ b/user/templates/index.hbs @@ -1,5 +1,9 @@