diff --git a/.gitignore b/.gitignore index 5977731..0645fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules coverage.html *.sqlite -config/config.json diff --git a/.travis.yml b/.travis.yml index b3b1afc..f33990e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ -# please wake up, travis language: node_js node_js: - "0.10" before_script: - - cp config/config.json.example config/config.json + - mysql -e 'create database rssmtp;' + - ./node_modules/.bin/knex migrate:latest +env: NODE_ENV=ci diff --git a/Makefile b/Makefile index 3d1d98e..65e6fb2 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +clean: + rm -rf node_modules + coverage: NODE_ENV=test \ DB_TOKEN="coverage" \ @@ -36,7 +39,7 @@ test: APP_SMTP_SSL="true" \ APP_SMTP_FROM="no-reply@example.com" \ APP_SMTP_PASS="dummy password" \ - ./node_modules/.bin/mocha --recursive test -R list + ./node_modules/.bin/mocha --recursive test -R dot testwatch: DB_TOKEN="testwatch" ./node_modules/.bin/chicken -c 'clear; time make test' . @@ -44,4 +47,4 @@ testwatch: install: npm install -.PHONY: cov coverage dev supper test testwatch +.PHONY: clean cov coverage dev supper test testwatch diff --git a/agents/fetcher.js b/agents/fetcher.js new file mode 100644 index 0000000..91b92de --- /dev/null +++ b/agents/fetcher.js @@ -0,0 +1,30 @@ +'use strict'; + +function Fetcher(getter) { + this._getter = getter; +} + +Fetcher.prototype.fetchFeed = function fetchFeed(feed, done) { + var getter = this._getter; + + feed.set('last_fetched', new Date()); + feed + .save() + .exec(function(err) { + if (err) { + return done(err); + } + + var url = feed.get('url'); + + getter(url, function(err, response, body) { + if (err) { + return done(err); + } + + done(); + }); + }); +}; + +module.exports = Fetcher; diff --git a/agents/index.js b/agents/index.js new file mode 100644 index 0000000..b4f3e97 --- /dev/null +++ b/agents/index.js @@ -0,0 +1,17 @@ +var _str = require('underscore.string'); +var fs = require('fs'); +var path = require('path'); + +var files = fs.readdirSync(__dirname); + +files.forEach(function(file) { + if (file === path.basename(__filename)) { + return; + } + + var class_name = _str.classify(path.basename(file, '.js')); + + var model = require('./' + file); + + module.exports[class_name] = model; +}); diff --git a/models/mailer.js b/agents/mailer.js similarity index 93% rename from models/mailer.js rename to agents/mailer.js index b434b8c..83ef2b6 100644 --- a/models/mailer.js +++ b/agents/mailer.js @@ -1,5 +1,4 @@ -var nodemailer = require('nodemailer') -; +var nodemailer = require('nodemailer'); function Mailer() { var settings = { diff --git a/models/poller.js b/agents/poller.js similarity index 100% rename from models/poller.js rename to agents/poller.js diff --git a/bookmodels/article.js b/bookmodels/article.js new file mode 100644 index 0000000..0ccb617 --- /dev/null +++ b/bookmodels/article.js @@ -0,0 +1,17 @@ +'use strict'; + +var db = require('../db'); + +var Article = db.BaseModel.extend({ + tableName: 'articles', + defaults: function defaults() { + return { + link: null, + description: null, + title: 'untitled article' + } + } +}, { +}); + +module.exports = Article; diff --git a/bookmodels/feed.js b/bookmodels/feed.js new file mode 100644 index 0000000..d2fae4a --- /dev/null +++ b/bookmodels/feed.js @@ -0,0 +1,10 @@ +'use strict'; + +var db = require('../db'); + +var Feed = db.BaseModel.extend({ + tableName: 'feeds' +}, { +}); + +module.exports = Feed; diff --git a/bookmodels/index.js b/bookmodels/index.js new file mode 100644 index 0000000..b4f3e97 --- /dev/null +++ b/bookmodels/index.js @@ -0,0 +1,17 @@ +var _str = require('underscore.string'); +var fs = require('fs'); +var path = require('path'); + +var files = fs.readdirSync(__dirname); + +files.forEach(function(file) { + if (file === path.basename(__filename)) { + return; + } + + var class_name = _str.classify(path.basename(file, '.js')); + + var model = require('./' + file); + + module.exports[class_name] = model; +}); diff --git a/bookmodels/user.js b/bookmodels/user.js new file mode 100644 index 0000000..c29b156 --- /dev/null +++ b/bookmodels/user.js @@ -0,0 +1,40 @@ +'use strict'; + +var db = require('../db'); + +var User = db.BaseModel.extend({ + tableName: 'users' +}, { + findOrCreateFromOAUTH: function findOrCreateFromOAUTH(oauth_data, done) { + var attrs = { + oauth_provider: oauth_data.provider, + oauth_id: oauth_data.id + }; + + var created = false; + var user = User + .forge(attrs) + + user + .fetch() + .then(function(result) { + if (result) { + return result; + } + + created = true; + user.set('email', oauth_data.emails[0].value); + + return user.save(); + }) + .exec(function(err, user) { + if (err) { + return done(err); + } + + done(null, user, created); + }); + } +}); + +module.exports = User; diff --git a/config/ci.js b/config/ci.js new file mode 100644 index 0000000..3f0aeb4 --- /dev/null +++ b/config/ci.js @@ -0,0 +1,17 @@ +module.exports = { + database: { + // for sequelize + dialect: "mysql", + username: "travis", + password: null, + database: "rssmtp", + host: "127.0.0.1", + + // for bookshelf + client: 'mysql2', + connection: { + database: 'rssmtp', + user: 'travis' + } + } +}; diff --git a/config/config.json.example b/config/config.json.example deleted file mode 100644 index c309367..0000000 --- a/config/config.json.example +++ /dev/null @@ -1,23 +0,0 @@ -{ - "test": { - "dialect": "sqlite", - "username": "root", - "password": null, - "database": "database_test", - "host": "127.0.0.1" - }, - "development": { - "dialect": "postgres", - "username": "rssmtp", - "password": "password_here", - "database": "rssmtp_development", - "host": "127.0.0.1" - }, - "production": { - "dialect": "postgres", - "username": "rssmtp", - "password": "password_here", - "database": "rssmtp_production", - "host": "127.0.0.1" - } -} diff --git a/config/default.js b/config/default.js new file mode 100644 index 0000000..21c8a39 --- /dev/null +++ b/config/default.js @@ -0,0 +1,19 @@ +module.exports = { + database: { + // for sequelize + dialect: "mysql", + username: "root", + password: null, + database: "rssmtp", + host: "127.0.0.1", + + //debug: true, + + // for bookshelf + client: 'mysql2', + connection: { + database: 'rssmtp', + user: 'root' + } + } +}; diff --git a/config/dev.js b/config/dev.js new file mode 100644 index 0000000..7be35b6 --- /dev/null +++ b/config/dev.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/config/prod.js b/config/prod.js new file mode 100644 index 0000000..b9518af --- /dev/null +++ b/config/prod.js @@ -0,0 +1,19 @@ +module.exports = { + database: { + // for sequelize + dialect: "postgres", + username: "rssmtp", + password: "password_here", + database: "rssmtp_production", + host: "127.0.0.1", + + // for bookshelf + client: 'pg', + connection: { + host: "127.0.0.1", + username: "rssmtp", + password: "password_here", + database: "rssmtp_production" + } + } +}; diff --git a/config/runtime.json b/config/runtime.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/config/runtime.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/config/test.js b/config/test.js new file mode 100644 index 0000000..7be35b6 --- /dev/null +++ b/config/test.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/db.js b/db.js new file mode 100644 index 0000000..734f197 --- /dev/null +++ b/db.js @@ -0,0 +1,62 @@ +var _ = require('lodash'); +var Bookshelf = require('bookshelf'); +var config = require('config'); +var Knex = require('knex'); + +var db_config = _.cloneDeep(config.database); +if (db_config.client.match(/^mysql/)) { + // make mysql pay attention to schema and not "just go with it" + db_config.pool = { + afterCreate: function(connection, callback) { + connection.query("SET sql_MODE='STRICT_ALL_TABLES';", function(err) { + callback(err, connection); + }); + } + }; +} + +var knex = Knex(db_config); +var bookshelf = Bookshelf.initialize(knex); + +bookshelf.plugin('registry'); +var Parent = bookshelf.Model; +var BaseModel = Parent.extend({ + initialize: function initialize(attrs, options) { + Parent.prototype.initialize.call(this, attrs, options); + + this.on('saving', this.onSaving); + }, + parse: function parse(input, options) { + var attrs = Parent.prototype.parse.call(this, input, options); + + (this.dateColumns || []).forEach(function(column) { + if (null != attrs[column]) { + attrs[column] = new Date(attrs[column]); + } + }); + + return attrs; + }, + onSaving: function onSaving(model, attrs, options) { + trim_milliseconds = this.trimMilliseconds; + + (this.dateColumns || []).forEach(function(column) { + if (null != attrs[column]) { + attrs[column] = new Date(attrs[column]); + + if (trim_milliseconds) { + attrs[column].setMilliseconds(0); + model.set(column, attrs[column]); + } + } + }); + }, + dateColumns: ['created_at', 'updated_at', 'last_fetched'], + trimMilliseconds: true, + hasTimestamps: true, +}, { +}); + +module.exports.knex = knex; +module.exports.bookshelf = bookshelf; +module.exports.BaseModel = BaseModel; diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 0000000..5cd1f72 --- /dev/null +++ b/knexfile.js @@ -0,0 +1,4 @@ +'use strict'; + +module.exports.development = require('config').database; +module.exports.ci = require('config').database; diff --git a/lib/promise.js b/lib/promise.js new file mode 100644 index 0000000..fcb8064 --- /dev/null +++ b/lib/promise.js @@ -0,0 +1,11 @@ +var BookPromise = require('bookshelf/lib/base/promise'); +var KnexPromise = require('knex/lib/promise'); + +function unhandledRejectionHandler(err) { + throw err; +} + +BookPromise.onPossiblyUnhandledRejection(unhandledRejectionHandler); +KnexPromise.onPossiblyUnhandledRejection(unhandledRejectionHandler); + +module.exports = KnexPromise; diff --git a/migrations/20140608000221_initial-migration.js b/migrations/20140608000221_initial-migration.js new file mode 100644 index 0000000..1b8e3c9 --- /dev/null +++ b/migrations/20140608000221_initial-migration.js @@ -0,0 +1,56 @@ + +exports.up = function(knex, Promise) { + var todo = []; + + todo.push(knex.schema.createTable('users', function(table){ + table.increments('id').primary().unsigned(); + + table.string('email',2048).notNullable(); + table.string('oauth_provider',2048).notNullable(); + table.string('oauth_id',2048).notNullable(); + + table.timestamps(); + })); + + todo.push(knex.schema.createTable('feeds', function(table){ + table.increments('id').primary().unsigned(); + + table.string('url',2048).notNullable(); + table.string('name',2048).notNullable(); + + table.datetime('last_fetched').notNullable(); + + table.timestamps(); + })); + + todo.push(knex.schema.createTable('feedsusers', function(table){ + table.increments('id').primary().unsigned(); + + table.integer('user_id').notNullable().unsigned(); + table.integer('feed_id').notNullable().unsigned(); + + table.index(['user_id','feed_id'], 'ix_user_id_feed_id'); + table.index(['feed_id','user_id'], 'ix_feed_id_user_id'); + + table.timestamps(); + })); + + todo.push(knex.schema.createTable('articles', function(table){ + table.increments('id').primary().unsigned(); + + table.datetime('date').notNullable(); + table.string('guid').notNullable(); + table.string('link',2048); + table.string('title',2048).notNullable().defaultTo('untitled article'); + table.text('description').defaultTo('this article does not have content'); + + table.integer('feed_id').notNullable().unsigned(); + + table.timestamps(); + })); + + return Promise.all(todo); +}; + +exports.down = function(knex, Promise) { +}; diff --git a/migrations/20150417160910_users-oauth-provider-id-are-unique.js b/migrations/20150417160910_users-oauth-provider-id-are-unique.js new file mode 100644 index 0000000..183bae2 --- /dev/null +++ b/migrations/20150417160910_users-oauth-provider-id-are-unique.js @@ -0,0 +1,19 @@ +'use strict'; + +exports.up = function(knex, Promise) { + return knex.schema.raw("ALTER TABLE users MODIFY COLUMN oauth_provider VARCHAR(255) NOT NULL") + .then(function() { + return knex.schema.raw("ALTER TABLE users MODIFY COLUMN oauth_id VARCHAR(255) NOT NULL") + }) + .then(function() { + return knex.schema.table('users', function(table) { + table.unique(['oauth_provider', 'oauth_id'], 'ux_oauth_provider_oauth_id'); + }); + }); +}; + +exports.down = function(knex, Promise) { + return knex.schema.table('users', function(table) { + table.dropUnique(['oauth_provider', 'oauth_id'], 'ux_oauth_provider_oauth_id'); + }); +}; diff --git a/models/Article.js b/models/Article.js index 9600a70..0f1490f 100644 --- a/models/Article.js +++ b/models/Article.js @@ -1,7 +1,5 @@ -var mmh3 = require('murmurhash3') - , _ = require('lodash') - , nodemailer = require('nodemailer') -; +var mmh3 = require('murmurhash3'); +var _ = require('lodash'); function init(Sequelize, sequelize, name, models) { var statics = {} @@ -75,8 +73,10 @@ function init(Sequelize, sequelize, name, models) { var cleaned = _.pick(input, _.keys(attrs)); delete cleaned['id']; - _(input).each(function(v,k){ - if (!v) delete cleaned[k]; + _.each(input, function(v,k){ + if (!v) { + delete cleaned[k]; + } }); return cleaned; diff --git a/models/Feed.js b/models/Feed.js index 85ccfee..5c859f0 100644 --- a/models/Feed.js +++ b/models/Feed.js @@ -1,9 +1,8 @@ -var request = require('request') - , feedparser = require('../lib/feedparser') - , moment = require('moment') - , async = require('async') - , _ = require('lodash') -; +var request = require('request'); +var feedparser = require('../lib/feedparser'); +var moment = require('moment'); +var async = require('async'); +var _ = require('lodash'); function init(Sequelize, sequelize, name, models) { var statics = {} diff --git a/models/index.js b/models/index.js index 0544142..99e42e9 100644 --- a/models/index.js +++ b/models/index.js @@ -2,11 +2,11 @@ var Sequelize = require('sequelize') , path = require('path') , fs = require('fs') , env = process.env.NODE_ENV || 'development' - , config = require(__dirname + '/../config/config.json')[env] + , config = require('config') , _ = require('lodash') ; -var options = _.merge({}, config, { +var options = _.merge({}, config.database, { define: { underscored: true , freezeTableName: true @@ -14,7 +14,7 @@ var options = _.merge({}, config, { , logging: false }); -var sequelize = new Sequelize(config.database, config.username, config.password, options); +var sequelize = new Sequelize(config.database.database, config.database.username, config.database.password, options); var files = fs.readdirSync(__dirname) .filter(function(filename){ diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index 79cf454..0000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,644 +0,0 @@ -{ - "name": "rssmtp", - "version": "0.0.1", - "dependencies": { - "express": { - "version": "3.4.6", - "from": "express@~3", - "dependencies": { - "commander": { - "version": "1.3.2", - "from": "commander@1.3.2", - "dependencies": { - "keypress": { - "version": "0.1.0", - "from": "keypress@0.1.x" - } - } - }, - "range-parser": { - "version": "0.0.4", - "from": "range-parser@0.0.4" - }, - "mkdirp": { - "version": "0.3.5", - "from": "mkdirp@0.3.5" - }, - "buffer-crc32": { - "version": "0.2.1", - "from": "buffer-crc32@0.2.1" - }, - "fresh": { - "version": "0.2.0", - "from": "fresh@0.2.0" - }, - "methods": { - "version": "0.1.0", - "from": "methods@0.1.0" - }, - "send": { - "version": "0.1.4", - "from": "send@0.1.4", - "dependencies": { - "mime": { - "version": "1.2.11", - "from": "mime@~1.2.9" - } - } - }, - "cookie-signature": { - "version": "1.0.1", - "from": "cookie-signature@1.0.1" - }, - "debug": { - "version": "0.7.4", - "from": "debug@>= 0.7.3 < 1" - } - } - }, - "jade": { - "version": "0.35.0", - "from": "jade@*", - "dependencies": { - "commander": { - "version": "2.0.0", - "from": "commander@~2.0.0" - }, - "mkdirp": { - "version": "0.3.5", - "from": "mkdirp@0.3.x" - }, - "transformers": { - "version": "2.1.0", - "from": "transformers@2.1.0", - "dependencies": { - "promise": { - "version": "2.0.0", - "from": "promise@~2.0", - "dependencies": { - "is-promise": { - "version": "1.0.0", - "from": "is-promise@~1" - } - } - }, - "css": { - "version": "1.0.8", - "from": "css@~1.0.8", - "dependencies": { - "css-parse": { - "version": "1.0.4", - "from": "css-parse@1.0.4" - }, - "css-stringify": { - "version": "1.0.5", - "from": "css-stringify@1.0.5" - } - } - }, - "uglify-js": { - "version": "2.2.5", - "from": "uglify-js@~2.2.5", - "dependencies": { - "source-map": { - "version": "0.1.31", - "from": "source-map@~0.1.7", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4" - } - } - }, - "optimist": { - "version": "0.3.7", - "from": "optimist@~0.3.5", - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "from": "wordwrap@~0.0.2" - } - } - } - } - } - } - }, - "character-parser": { - "version": "1.2.0", - "from": "character-parser@1.2.0" - }, - "monocle": { - "version": "1.1.50", - "from": "monocle@1.1.50", - "dependencies": { - "readdirp": { - "version": "0.2.5", - "from": "readdirp@~0.2.3", - "dependencies": { - "minimatch": { - "version": "0.2.12", - "from": "minimatch@>=0.2.4", - "dependencies": { - "lru-cache": { - "version": "2.5.0", - "from": "lru-cache@2" - }, - "sigmund": { - "version": "1.0.0", - "from": "sigmund@~1.0.0" - } - } - } - } - } - } - }, - "with": { - "version": "1.1.1", - "from": "with@~1.1.0", - "dependencies": { - "uglify-js": { - "version": "2.4.0", - "from": "uglify-js@2.4.0", - "dependencies": { - "source-map": { - "version": "0.1.31", - "from": "source-map@~0.1.7", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4" - } - } - }, - "optimist": { - "version": "0.3.7", - "from": "optimist@~0.3.5", - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "from": "wordwrap@~0.0.2" - } - } - }, - "uglify-to-browserify": { - "version": "1.0.1", - "from": "uglify-to-browserify@~1.0.0" - } - } - } - } - }, - "constantinople": { - "version": "1.0.2", - "from": "constantinople@~1.0.1", - "dependencies": { - "uglify-js": { - "version": "2.4.6", - "from": "uglify-js@~2.4.0", - "dependencies": { - "source-map": { - "version": "0.1.31", - "from": "source-map@~0.1.7", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4" - } - } - }, - "optimist": { - "version": "0.3.7", - "from": "optimist@~0.3.5", - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "from": "wordwrap@~0.0.2" - } - } - }, - "uglify-to-browserify": { - "version": "1.0.1", - "from": "uglify-to-browserify@~1.0.0" - } - } - } - } - } - } - }, - "feedparser": { - "version": "0.16.3", - "from": "feedparser@~0.16", - "dependencies": { - "sax": { - "version": "0.5.5", - "from": "sax@0.5.x" - }, - "addressparser": { - "version": "0.1.3", - "from": "addressparser@~0.1.3" - }, - "array-indexofobject": { - "version": "0.0.1", - "from": "array-indexofobject@0.0.1" - }, - "readable-stream": { - "version": "1.0.17", - "from": "readable-stream@1.0.x" - }, - "resanitize": { - "version": "0.3.0", - "from": "resanitize@~0.3.0", - "dependencies": { - "validator": { - "version": "1.5.1", - "from": "validator@~1.5.1" - } - } - } - } - }, - "nodemailer": { - "version": "0.5.13", - "from": "nodemailer@~0.5", - "dependencies": { - "mailcomposer": { - "version": "0.2.6", - "from": "mailcomposer@~0.2.5", - "dependencies": { - "mimelib": { - "version": "0.2.14", - "from": "mimelib@~0.2.14", - "dependencies": { - "encoding": { - "version": "0.1.7", - "from": "encoding@~0.1", - "dependencies": { - "iconv-lite": { - "version": "0.2.11", - "from": "iconv-lite@~0.2.11" - } - } - }, - "addressparser": { - "version": "0.2.0", - "from": "addressparser@~0.2.0" - } - } - }, - "mime": { - "version": "1.2.9", - "from": "mime@1.2.9" - }, - "punycode": { - "version": "1.2.3", - "from": "punycode@>=0.2.0" - }, - "dkim-signer": { - "version": "0.1.0", - "from": "dkim-signer@~0.1.0" - } - } - }, - "simplesmtp": { - "version": "0.3.16", - "from": "simplesmtp@~0.2 || ~0.3", - "dependencies": { - "rai": { - "version": "0.1.8", - "from": "rai@~0.1" - }, - "xoauth2": { - "version": "0.1.8", - "from": "xoauth2@~0.1" - } - } - }, - "directmail": { - "version": "0.1.5", - "from": "directmail@~0.1.1" - }, - "he": { - "version": "0.3.6", - "from": "he@~0.3.6" - }, - "readable-stream": { - "version": "1.1.9", - "from": "readable-stream@*", - "dependencies": { - "core-util-is": { - "version": "1.0.0", - "from": "core-util-is@~1.0.0" - }, - "debuglog": { - "version": "0.0.2", - "from": "debuglog@0.0.2" - } - } - } - } - }, - "async": { - "version": "0.2.9", - "from": "async@~0" - }, - "request": { - "version": "2.28.0", - "from": "request@~2", - "dependencies": { - "qs": { - "version": "0.6.6", - "from": "qs@~0.6.0" - }, - "json-stringify-safe": { - "version": "5.0.0", - "from": "json-stringify-safe@~5.0.0" - }, - "forever-agent": { - "version": "0.5.0", - "from": "forever-agent@~0.5.0" - }, - "node-uuid": { - "version": "1.4.1", - "from": "node-uuid@~1.4.0" - }, - "mime": { - "version": "1.2.11", - "from": "mime@~1.2.9" - }, - "tough-cookie": { - "version": "0.9.15", - "from": "tough-cookie@~0.9.15", - "dependencies": { - "punycode": { - "version": "1.2.3", - "from": "punycode@>=0.2.0" - } - } - }, - "form-data": { - "version": "0.1.2", - "from": "form-data@~0.1.0", - "dependencies": { - "combined-stream": { - "version": "0.0.4", - "from": "combined-stream@~0.0.4", - "dependencies": { - "delayed-stream": { - "version": "0.0.5", - "from": "delayed-stream@0.0.5" - } - } - } - } - }, - "tunnel-agent": { - "version": "0.3.0", - "from": "tunnel-agent@~0.3.0" - }, - "http-signature": { - "version": "0.10.0", - "from": "http-signature@~0.10.0", - "dependencies": { - "assert-plus": { - "version": "0.1.2", - "from": "assert-plus@0.1.2" - }, - "asn1": { - "version": "0.1.11", - "from": "asn1@0.1.11" - }, - "ctype": { - "version": "0.5.2", - "from": "ctype@0.5.2" - } - } - }, - "oauth-sign": { - "version": "0.3.0", - "from": "oauth-sign@~0.3.0" - }, - "hawk": { - "version": "1.0.0", - "from": "hawk@~1.0.0", - "dependencies": { - "hoek": { - "version": "0.9.1", - "from": "hoek@0.9.x" - }, - "boom": { - "version": "0.4.2", - "from": "boom@0.4.x" - }, - "cryptiles": { - "version": "0.2.2", - "from": "cryptiles@0.2.x" - }, - "sntp": { - "version": "0.2.4", - "from": "sntp@0.2.x" - } - } - }, - "aws-sign2": { - "version": "0.5.0", - "from": "aws-sign2@~0.5.0" - } - } - }, - "passport": { - "version": "0.1.17", - "from": "passport@~0.1.16", - "dependencies": { - "pkginfo": { - "version": "0.2.3", - "from": "pkginfo@0.2.x" - }, - "pause": { - "version": "0.0.1", - "from": "pause@0.0.1" - } - } - }, - "passport-google-oauth": { - "version": "0.1.5", - "from": "passport-google-oauth@~0.1.5", - "dependencies": { - "pkginfo": { - "version": "0.2.3", - "from": "pkginfo@0.2.x" - }, - "passport-oauth": { - "version": "0.1.15", - "from": "passport-oauth@~0.1.4", - "dependencies": { - "oauth": { - "version": "0.9.10", - "from": "oauth@0.9.x" - } - } - } - } - }, - "express-namespace": { - "version": "0.1.1", - "from": "express-namespace@~0.1.1", - "dependencies": { - "methods": { - "version": "0.0.1", - "from": "methods@0.0.1" - } - } - }, - "underscore": { - "version": "1.5.2", - "from": "underscore@~1" - }, - "moment": { - "version": "2.4.0", - "from": "moment@~2" - }, - "murmurhash3": { - "version": "0.2.3", - "from": "murmurhash3@~0.2", - "resolved": "https://registry.npmjs.org/murmurhash3/-/murmurhash3-0.2.3.tgz" - }, - "connect-flash": { - "version": "0.1.1", - "from": "connect-flash@~0.1" - }, - "sequelize": { - "version": "1.7.0-beta.1", - "from": "sequelize@git+https://github.com/elliotf/sequelize.git#elliotf_customized", - "resolved": "git+https://github.com/elliotf/sequelize.git#0d4bdc9874d3d2ac602e28064796f1ca4eb15c6d", - "dependencies": { - "lodash": { - "version": "2.2.1", - "from": "lodash@~2.2.0" - }, - "underscore.string": { - "version": "2.3.3", - "from": "underscore.string@~2.3.0" - }, - "lingo": { - "version": "0.0.5", - "from": "lingo@~0.0.5" - }, - "validator": { - "version": "1.5.1", - "from": "validator@~1.5.1" - }, - "moment": { - "version": "2.2.1", - "from": "moment@~2.2.1" - }, - "commander": { - "version": "2.0.0", - "from": "commander@~2.0.0" - }, - "dottie": { - "version": "0.0.8-0", - "from": "dottie@0.0.8-0" - }, - "toposort-class": { - "version": "0.2.1", - "from": "toposort-class@~0.2.0" - }, - "generic-pool": { - "version": "2.0.4", - "from": "generic-pool@2.0.4" - }, - "promise": { - "version": "3.2.0", - "from": "promise@~3.2.0" - }, - "sql": { - "version": "0.28.0", - "from": "sql@~0.28.0", - "dependencies": { - "sliced": { - "version": "0.0.5", - "from": "sliced@0.0.x" - }, - "lodash": { - "version": "1.3.1", - "from": "lodash@1.3.x" - } - } - } - } - }, - "lodash": { - "version": "2.4.1", - "from": "lodash@~2" - }, - "pg": { - "version": "2.8.3", - "from": "pg@~2", - "dependencies": { - "generic-pool": { - "version": "2.0.3", - "from": "generic-pool@2.0.3" - }, - "buffer-writer": { - "version": "1.0.0", - "from": "buffer-writer@1.0.0", - "dependencies": { - "cloned": { - "version": "0.0.1", - "from": "cloned@0.0.1", - "dependencies": { - "rmdir": { - "version": "1.0.0", - "from": "rmdir@~1.0.0" - } - } - } - } - } - } - }, - "commander": { - "version": "2.1.0", - "from": "commander@~2" - }, - "prettyjson": { - "version": "0.9.0", - "from": "prettyjson@~0", - "dependencies": { - "colors": { - "version": "0.6.0-1", - "from": "colors@0.6.0-1" - } - } - }, - "resumer": { - "version": "0.0.0", - "from": "resumer@0.0.0", - "dependencies": { - "through": { - "version": "2.3.4", - "from": "through@~2.3.4" - } - } - }, - "sinon": { - "version": "1.7.3", - "from": "sinon@>=1.4.0 <2", - "dependencies": { - "buster-format": { - "version": "0.5.6", - "from": "buster-format@~0.5", - "dependencies": { - "buster-core": { - "version": "0.6.4", - "from": "buster-core@=0.6.4" - } - } - } - } - } - } -} diff --git a/package.json b/package.json index 3690b62..c67606b 100644 --- a/package.json +++ b/package.json @@ -10,38 +10,46 @@ }, "dependencies": { "async": "0.2.9", + "bookshelf": "0.7.9", "commander": "2.1.0", + "config": "0.4.36", "connect-flash": "0.1.1", "express": "3.4.6", "express-namespace": "0.1.1", "feedparser": "0.16.3", - "jade": "0.35.0", - "lodash": "2.4.1", - "moment": "2.4.0", + "jade": "1.9.2", + "knex": "0.7.6", + "lodash": "3.7.0", + "moment": "2.10.2", "murmurhash3": "0.2.3", + "mysql": "2.6.2", + "mysql2": "0.15.5", "nodemailer": "0.5.13", "passport": "0.1.17", "passport-google-oauth": "0.1.5", - "pg": "2.8.3", + "pg": "4.3.0", "prettyjson": "0.9.0", - "request": "2.28.0", + "request": "2.55.0", "resumer": "0.0.0", "sequelize": "1.7.0", - "underscore": "1.5.2" + "supertest": "0.15.0", + "underscore": "1.8.3", + "underscore.string": "3.0.3" }, "devDependencies": { "blanket": "1.1.5", - "chai": "^1.9.1", + "chai": "2.2.0", "chai-fuzzy": "1.3.0", - "cheerio": "0.12.4", + "cheerio": "0.19.0", "chicken-little": "0.1.2", "connect": "2.11.2", "cookie": "0.1.0", + "dirty-chai": "1.2.0", "mocha": "1.15.1", "mocha-sinon": "1.0.0", "nodemon": "0.7.2", - "sinon-chai": "2.4.0", - "sqlite3": "2.1.19", + "sinon-chai": "2.7.0", + "sqlite3": "3.0.5", "supertest": "0.8.2" } } diff --git a/server.js b/server.js index 8a0f03d..5d1b345 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ -var http = require('http') - , app = require('./app') - , models = require('./models') -; +var http = require('http'); +var app = require('./app'); +var models = require('./models'); +var agents = require('./agents'); function start(done) { http.createServer(app).listen(app.get('port'), app.get('bindip'), function(){ @@ -15,9 +15,9 @@ function start(done) { models._sequelize.sync(syncArgs).done(function(err){ if (err) throw err; - var poller = new models.poller({ + var poller = new agents.Poller({ FeedClass: models.Feed - , mailer: new models.mailer() + , mailer: new agents.Mailer() }); poller.start(); diff --git a/support/spec_helper.js b/support/spec_helper.js index 8d6b72a..eb57b49 100644 --- a/support/spec_helper.js +++ b/support/spec_helper.js @@ -1,16 +1,20 @@ -var app = require('../app') - , request = require('supertest') - , chai = require('chai') - , cheerio = require('cheerio') - , async = require('async') - , _ = require('lodash') - , models = require('../models') -; +var _ = require('lodash'); +var app = require('../app'); +var async = require('async'); +var chai = require('chai'); +var cheerio = require('cheerio'); +var config = require('config'); +var db = require('../db'); +var models = require('../models'); +var Promise = require('../lib/promise'); +var request = require('supertest'); +var expect = chai.expect; require('mocha-sinon'); // chai setup -chai.Assertion.includeStack = true; +chai.config.includeStack = true; +//chai.use(require('dirty-chai')); chai.use(require('chai-fuzzy')); chai.use(require('sinon-chai')); @@ -51,22 +55,32 @@ exports.model = function(model) { return require('../models/' + model); }; -before(function(done){ - models._sequelize.sync({force: true}).done(done); -}); +before(function(done) { + this.timeout(30 * 1000); -beforeEach(function(done) { - var todo = []; + db.knex.migrate + .latest(config.database) + .exec(function(err) { + expect(err).to.not.exist; - _.forEach(models, function(model, name){ - if (model && 'function' === typeof model['destroy']) { - todo.push(function(done){ - model.destroy().done(done); - }); - } - }); + setTimeout(done, 20); + }); +}); - async.parallel(todo, done); +beforeEach(function(done) { + var tables_to_clear = ['articles', 'feeds', 'feedsusers', 'users']; + + Promise + .map(tables_to_clear, function(table_name) { + return db + .knex(table_name) + .del(); + }) + .exec(function(err) { + expect(err).to.not.exist; + + done(); + }); }); beforeEach(function(done) { @@ -77,7 +91,9 @@ beforeEach(function(done) { todo.push(function(done){ models.User .create({ - email: 'default_user@example.com' + email: 'default_user@example.com', + oauth_provider: 'fake_oauth_provider', + oauth_id: 'default user' }) .error(done) .success(function(user){ @@ -89,7 +105,9 @@ beforeEach(function(done) { todo.push(function(done){ models.User .create({ - email: 'other_user@example.com' + email: 'other_user@example.com', + oauth_provider: 'fake_oauth_provider', + oauth_id: 'other user' }) .error(done) .success(function(user){ diff --git a/test/agents.fetcher.js b/test/agents.fetcher.js new file mode 100644 index 0000000..17a780b --- /dev/null +++ b/test/agents.fetcher.js @@ -0,0 +1,125 @@ +'use strict'; + +var _ = require('lodash'); +var request = require('request'); +var expect = require('chai').expect; +var helper = require('../support/spec_helper'); +var agents = require('../agents'); +var feedparser = require('../lib/feedparser'); +var models = require('../bookmodels'); + +describe("agents.Fetcher", function() { + var fetcher; + var feed_title; + var fake_http_err; + var fake_http_response; + var fake_http_body; + + beforeEach(function() { + feed_title = 'a title'; + + fake_http_err = null; //{fake: 'HttpErr'}; + fake_http_response = {fake: 'HttpResponse'}; + fake_http_body = [ + '', + '', + 'a title', + '' + ].join(''); + + this.sinon.stub(request, 'get') + .yields(fake_http_err, fake_http_response, fake_http_body); + + this.sinon.spy(feedparser, 'parseString'); + + fetcher = new agents.Fetcher(request.get); + }); + + describe('#fetchFeed', function() { + var feed; + + beforeEach(function(done) { + var attrs = { + url: 'http://example.com/rss.xml', + name: 'an fake feed', + last_fetched: new Date('2010-01-01T00:00:00.000Z') + }; + + models.Feed + .forge(attrs) + .save() + .exec(function(err, result) { + expect(err).to.not.exist; + + feed = result; + + done(); + }); + }); + + it('updates the `last_fetched` timestamp', function(done) { + var now = new Date(); + now.setMilliseconds(0); + var clock = this.sinon.useFakeTimers(now.valueOf()); + fetcher.fetchFeed(feed, function(err, updated, articles) { + clock.restore(); + expect(err).to.not.exist; + + models.Feed.fetchAll().exec(function(err, feeds) { + expect(err).to.not.exist; + + expect(feed.get('last_fetched')).to.deep.equal(now); + + var expected = feed.toJSON(); + expected.last_fetched = now; + expect(feeds.toJSON()).to.deep.equal([expected]); + + done(); + }); + }); + }); + + it('fetches the feed\'s url', function(done) { + fetcher.fetchFeed(feed, function(err, updated, articles) { + expect(err).to.not.exist; + + expect(request.get).to.have.been.calledOnce; + expect(request.get).to.have.been.calledWith('http://example.com/rss.xml'); + + done(); + }); + }); + + context.skip('when there are articles', function() { + context('and some are new', function() { + it('publishes them', function(done) { + }); + }); + + context('and none are new', function() { + }); + }); + + context.skip('when the feed\'s url has changed', function() { + it('updates the feed\'s url', function(done) { + }); + }); + }); + + describe.skip('#getAvailableFeeds', function() { + context('when the url contains pointers to feeds', function() { + it('returns an array of available feeds', function(done) { + }); + }); + + context('when the url contains no feeds', function() { + }); + + context('when the url is not HTML', function() { + context('but is a feed', function() { + it('returns the feed data', function(done) { + }); + }); + }); + }); +}); diff --git a/test/models/mailer.js b/test/agents.mailer.js similarity index 74% rename from test/models/mailer.js rename to test/agents.mailer.js index df6f200..6ce98a8 100644 --- a/test/models/mailer.js +++ b/test/agents.mailer.js @@ -1,12 +1,10 @@ -var helper = require('../../support/spec_helper') - , models = require('../../models') - , Mailer = helper.model('mailer') - , expect = require('chai').expect - , _ = require('lodash') - , nodemailer = require('nodemailer') -; - -describe("Mailer model", function() { +var helper = require('../support/spec_helper'); +var agents = require('../agents'); +var expect = require('chai').expect; +var _ = require('lodash'); +var nodemailer = require('nodemailer'); + +describe("agents.Mailer", function() { beforeEach(function() { var mockedMailer = this.mockedMailer = nodemailer.createTransport(); @@ -16,7 +14,7 @@ describe("Mailer model", function() { }); it("is a wrapper around nodemailer", function() { - var mailer = new Mailer(); + var mailer = new agents.Mailer(); expect(nodemailer.createTransport).to.have.been.calledWith("SMTP", { host: "smtp.example.com" @@ -31,7 +29,7 @@ describe("Mailer model", function() { describe("#sendMail", function() { beforeEach(function() { - this.mailer = new Mailer(); + this.mailer = new agents.Mailer(); }); it("sends the given email via nodemailer", function(done) { diff --git a/test/models/poller.js b/test/agents.poller.js similarity index 86% rename from test/models/poller.js rename to test/agents.poller.js index 8c19bd1..711d204 100644 --- a/test/models/poller.js +++ b/test/agents.poller.js @@ -1,19 +1,17 @@ -var helper = require('../../support/spec_helper') - , expect = require('chai').expect - , Poller = helper.model('poller') - , models = require('../../models') - , Feed = models.Feed -; - -describe("Poller model", function() { +var helper = require('../support/spec_helper'); +var expect = require('chai').expect; +var models = require('../models'); +var agents = require('../agents'); + +describe("agents.Poller", function() { beforeEach(function() { this.fakeMailer = { sendMail: this.sinon.stub() }; this.fakeMailer.sendMail.callsArg(1) - this.poller = new Poller({ - FeedClass: Feed + this.poller = new agents.Poller({ + FeedClass: models.Feed , mailer: this.fakeMailer }); this.sinon.stub(this.poller, 'requeue', function(){}); @@ -23,7 +21,7 @@ describe("Poller model", function() { beforeEach(function(done) { var self = this; - Feed + models.Feed .create({ url: 'http://example.com/#updateOneFeed' }) @@ -35,7 +33,7 @@ describe("Poller model", function() { done(); }); - this.sinon.stub(Feed, 'getOutdated', function(done){ + this.sinon.stub(models.Feed, 'getOutdated', function(done){ done(null, this.feed); }.bind(this)); }); @@ -44,7 +42,7 @@ describe("Poller model", function() { this.poller.updateOneFeed(function(err, feed){ expect(err).to.not.exist; - expect(Feed.getOutdated).to.have.been.called; + expect(models.Feed.getOutdated).to.have.been.called; expect(this.feed.publish).to.have.been.called; expect(this.feed.publish).to.have.been.calledWith(this.fakeMailer); diff --git a/test/bookmodels.article.js b/test/bookmodels.article.js new file mode 100644 index 0000000..bbc9927 --- /dev/null +++ b/test/bookmodels.article.js @@ -0,0 +1,86 @@ +'use strict'; + +var expect = require('chai').expect; +var models = require('../bookmodels'); +var helper = require('../support/spec_helper'); + +describe("models.Article (bookshelf)", function() { + var minimum_attrs; + + beforeEach(function() { + minimum_attrs = { + feed_id: 0, + date: new Date('2010-01-01T00:00:00.000Z'), + guid: 'a fake guid' + }; + }); + + describe('#save', function() { + context('when a date is not provided', function() { + it('yields an error', function(done) { + delete minimum_attrs.date; + + models.Article + .forge(minimum_attrs) + .save() + .exec(function(err, article) { + expect(err).to.exist; + + done(); + }); + }); + }); + }); + + describe('#defaults', function() { + context('when some attributes are not provided', function() { + it('generates attribute values', function(done) { + var now = new Date(); + now.setMilliseconds(0); + var clock = this.sinon.useFakeTimers(now.valueOf()); + models.Article + .forge(minimum_attrs) + .save() + .exec(function(err, article) { + clock.restore(); + + expect(err).to.not.exist; + + var actual = article.toJSON(); + expect(actual.id).to.be.a('number').above(0); + delete actual.id; + + expect(actual).to.deep.equal({ + feed_id: 0, + date: new Date('2010-01-01T00:00:00.000Z'), + guid: 'a fake guid', + link: null, + title: 'untitled article', + description: null, + created_at: now, + updated_at: now, + }); + + var actual = article.toJSON(); + expect(actual.id).to.be.a('number').above(0); + delete actual.id; + + models.Article + .fetchAll() + .exec(function(err, articles) { + expect(err).to.not.exist; + + expect(articles.toJSON()).to.deep.equal([article.toJSON()]); + + done(); + }); + }); + }); + }); + + context.skip('when there is no guid', function() { + it('generates one via hashing the article attributes', function(done) { + }); + }); + }); +}); diff --git a/test/bookmodels.feed.js b/test/bookmodels.feed.js new file mode 100644 index 0000000..9de7227 --- /dev/null +++ b/test/bookmodels.feed.js @@ -0,0 +1,47 @@ +'use strict'; + +var expect = require('chai').expect; +var models = require('../bookmodels'); +var helper = require('../support/spec_helper'); + +describe("models.Feed (bookshelf)", function() { + var minimum_attrs; + var now; + + beforeEach(function() { + now = new Date(); + now.setMilliseconds(0); + + minimum_attrs = { + url: 'http://example.com/rss.xml', + name: 'fake feed name', + last_fetched: now + }; + }); + + it('can be saved', function(done) { + var clock = this.sinon.useFakeTimers(now.valueOf()); + models.Feed + .forge(minimum_attrs) + .save() + .exec(function(err, user) { + clock.restore(); + + expect(err).to.not.exist; + + var actual = user.toJSON(); + expect(actual.id).to.be.a('number').above(0); + delete actual.id; + + expect(actual).to.deep.equal({ + url: 'http://example.com/rss.xml', + name: 'fake feed name', + last_fetched: now, + created_at: now, + updated_at: now, + }); + + done(); + }); + }); +}); diff --git a/test/bookmodels.user.js b/test/bookmodels.user.js new file mode 100644 index 0000000..afc17d7 --- /dev/null +++ b/test/bookmodels.user.js @@ -0,0 +1,108 @@ +'use strict'; + +var expect = require('chai').expect; +var models = require('../bookmodels'); +var helper = require('../support/spec_helper'); + +describe("models.User (bookshelf)", function() { + var minimum_attrs; + + beforeEach(function() { + minimum_attrs = { + email: 'fake@example.com', + oauth_provider: 'fake oauth_provider', + oauth_id: 'fake oauth_id' + }; + }); + + it('can be saved', function(done) { + var now = new Date(); + now.setMilliseconds(0); + var clock = this.sinon.useFakeTimers(now.valueOf()); + models.User + .forge(minimum_attrs) + .save() + .exec(function(err, user) { + clock.restore(); + + expect(err).to.not.exist; + + var actual = user.toJSON(); + expect(actual.id).to.be.a('number').above(0); + delete actual.id; + + expect(actual).to.deep.equal({ + email: 'fake@example.com', + oauth_provider: 'fake oauth_provider', + oauth_id: 'fake oauth_id', + created_at: now, + updated_at: now, + }); + + done(); + }); + }); + + describe("findOrCreateFromOAUTH", function() { + var dummyProfileData; + var user; + + beforeEach(function() { + dummyProfileData = { + provider: 'oauth_provider_here' + , id: 'oauth_id_here' + , displayName: 'Bob Foster' + , name: { + givenName: 'Bob' + , familyName: 'Foster' + } + , emails: [ + { value: 'bob.foster@example.com' } + ] + }; + }); + + context("when the specified user *does not* exist", function() { + it("creates the user", function(done) { + models.User.findOrCreateFromOAUTH(dummyProfileData, function(err, user, created) { + expect(err).to.not.exist; + + expect(created).to.be.true; + expect(user.pick('email', 'oauth_provider', 'oauth_id')).to.deep.equal({ + email: 'bob.foster@example.com', + oauth_provider: 'oauth_provider_here', + oauth_id: 'oauth_id_here' + }); + + done(); + }); + }); + }); + + context("when the specified user *does* exist", function() { + beforeEach(function(done) { + models.User.forge({ + email: 'bob@example.com' + , oauth_provider: 'oauth_provider_here' + , oauth_id: 'oauth_id_here' + }) + .save() + .exec(function(err, result){ + expect(err).to.not.exist; + user = result; + done(); + }); + }); + + it("returns the existing user", function(done) { + models.User.findOrCreateFromOAUTH(dummyProfileData, function(err, user, created) { + expect(err).to.not.exist; + + expect(created).to.be.false; + expect(user.get('email')).to.equal("bob@example.com"); + done(); + }); + }); + }); + }); +}); diff --git a/test/models/Article.js b/test/models.Article.js similarity index 71% rename from test/models/Article.js rename to test/models.Article.js index ed02e84..d2152ee 100644 --- a/test/models/Article.js +++ b/test/models.Article.js @@ -1,16 +1,16 @@ -var helper = require('../../support/spec_helper') - , models = require('../../models') - , User = models.User - , Feed = models.Feed - , Article = models.Article - , expect = require('chai').expect - , async = require('async') - , _ = require('lodash') - , mmh3 = require('murmurhash3') - , nodemailer = require('nodemailer') -; +var helper = require('../support/spec_helper'); +var models = require('../models'); +var User = models.User; +var Feed = models.Feed; +var Article = models.Article; +var expect = require('chai').expect; +var async = require('async'); +var _ = require('lodash'); +var mmh3 = require('murmurhash3'); describe("Article model (RDBMS)", function() { + var data; + beforeEach(function(done) { Feed.create({ url: "http://example.com/article.rss" @@ -24,23 +24,24 @@ describe("Article model (RDBMS)", function() { }); beforeEach(function() { - this.data = { + data = { title: "article title here: with " , description: "

article content here, with and &'s

" , link: 'http://example.com/an_article' , date: new Date(2010,0) , guid: 'a guid here' , feed_id: this.feed.id - } + }; }); it("can be saved", function(done) { - Article.create(this.data).done(done); + Article.create(data).done(done); }); - it("cannot be created with an invalid feed_id ", function(done) { - this.data.feed_id = 9000000; - Article.create(this.data).done(function(err, article){ + // I don't think we care about this at the moment + it.skip("cannot be created with an invalid feed_id ", function(done) { + data.feed_id = 9000000; + Article.create(data).done(function(err, article){ expect(err).to.exist; expect(err).to.match(/foreign key constraint/i); @@ -49,8 +50,9 @@ describe("Article model (RDBMS)", function() { }); it("cannot be created without a feed_id", function(done) { - delete this.data.feed_id; - Article.create(this.data).done(function(err, article){ + data.feed_id = null; + + Article.create(data).done(function(err, article){ expect(err).to.exist; expect(err).to.match(/null/i); expect(err).to.match(/feed_id/i); @@ -61,19 +63,19 @@ describe("Article model (RDBMS)", function() { describe(".cleanAttrs", function() { it("strips out unsupported attributes", function() { - this.data.id = 'a non numeric key that will be discarded'; - this.data.discarded = 'this will be thrown away'; + data.id = 'a non numeric key that will be discarded'; + data.discarded = 'this will be thrown away'; - var cleaned = Article.cleanAttrs(this.data); + var cleaned = Article.cleanAttrs(data); expect(_.keys(cleaned)).to.include('description'); expect(_.keys(cleaned)).to.not.include('discarded'); expect(_.keys(cleaned)).to.not.include('id'); }); it("removes empty attributes to allow defaults to be set", function() { - this.data.title = ''; + data.title = ''; - var cleaned = Article.cleanAttrs(this.data); + var cleaned = Article.cleanAttrs(data); expect(_.keys(cleaned)).to.include('description'); expect(_.keys(cleaned)).to.not.include('title'); }); @@ -82,16 +84,16 @@ describe("Article model (RDBMS)", function() { describe(".setDefaults", function() { describe("when there is no title", function() { beforeEach(function() { - delete this.data['title']; + delete data['title']; }); describe.skip("but there is content", function() { beforeEach(function() { - this.data.description = "

Article's content here & more will be truncated because it's too long"; + data.description = "

Article's content here & more will be truncated because it's too long"; }); it("sets the title to be the first N char of the content", function() { - var defaulted = Article.setDefaults(this.data); + var defaulted = Article.setDefaults(data); expect(defaulted.title + '').to.equal("Article's content here & more will be truncated because it's(...)"); }); @@ -99,18 +101,18 @@ describe("Article model (RDBMS)", function() { describe("but there is a link", function() { it("sets the title to be the first N char of the content", function() { - var defaulted = Article.setDefaults(this.data); + var defaulted = Article.setDefaults(data); - expect(defaulted.title).to.equal(this.data.link); + expect(defaulted.title).to.equal(data.link); }); }); }); it("does not modify pre-existing values", function() { - var defaulted = Article.setDefaults(this.data); + var defaulted = Article.setDefaults(data); - expect(defaulted === this.data).to.be.false; - expect(defaulted).to.deep.equal(this.data); + expect(defaulted === data).to.be.false; + expect(defaulted).to.deep.equal(data); }); it("cleans attributes before setting defaults", function() { @@ -118,9 +120,9 @@ describe("Article model (RDBMS)", function() { this.sinon.stub(Article, 'cleanAttrs', function() { return fakeCleaned}); - var defaulted = Article.setDefaults(this.data); + var defaulted = Article.setDefaults(data); - expect(Article.cleanAttrs).to.have.been.calledWith(this.data); + expect(Article.cleanAttrs).to.have.been.calledWith(data); expect(defaulted.fake).to.equal('cleaned'); }); @@ -141,14 +143,14 @@ describe("Article model (RDBMS)", function() { it("cleans the input before hashing", function(done) { this.sinon.stub(Article, 'setDefaults', function(){ return { fake: 'defaulted' };}); - Article.setGUID(this.data, function(err, attrs){ + Article.setGUID(data, function(err, attrs){ expect(err).to.not.exist; - expect(Article.setDefaults).to.have.been.calledWith(this.data); + expect(Article.setDefaults).to.have.been.calledWith(data); expect(mmh3.murmur128Hex).to.have.been.calledWith('fake: defaulted'); expect(attrs).to.be.ok; - expect(attrs).to.not.equal(this.data); + expect(attrs).to.not.equal(data); done(); }.bind(this)); @@ -156,7 +158,7 @@ describe("Article model (RDBMS)", function() { describe("when the input already has a guid", function() { it("does not overwrite it", function(done) { - Article.setGUID(this.data, function(err, attrs){ + Article.setGUID(data, function(err, attrs){ expect(err).to.not.exist; expect(attrs.guid).to.equal('a guid here'); @@ -169,11 +171,11 @@ describe("Article model (RDBMS)", function() { describe("when the input does not have a guid", function() { beforeEach(function() { - delete this.data['guid']; + delete data['guid']; }); it("generates a guid", function(done) { - Article.setGUID(this.data, function(err, attrs){ + Article.setGUID(data, function(err, attrs){ expect(err).to.not.exist; expect(attrs.guid).to.match(/[0-9a-f]{32}/); @@ -190,14 +192,14 @@ describe("Article model (RDBMS)", function() { it("calls sequelize's findOrCreate using processed input", function(done) { this.sinon.spy(Article, 'findOrCreate'); - Article.findOrCreateFromData(this.data, function(err, article, created){ + Article.findOrCreateFromData(data, function(err, article, created){ expect(err).to.not.exist; - expect(Article.findOrCreate).to.have.been.calledWith({guid: this.data.guid, feed_id: this.data.feed_id}, this.data); + expect(Article.findOrCreate).to.have.been.calledWith({guid: data.guid, feed_id: data.feed_id}, data); expect(created).to.be.true; expect(article).to.be.ok; - expect(article.title).to.equal(this.data.title); + expect(article.title).to.equal(data.title); done(); }.bind(this)); @@ -206,22 +208,23 @@ describe("Article model (RDBMS)", function() { describe("#asEmailOptions", function() { describe("when there is content", function() { + var article; + var emails; + beforeEach(function(done) { - var self = this - , todo = [] - ; + var todo = []; todo.push(function(done){ Article - .create(self.data) + .create(data) .error(done) - .success(function(article){ - self.article = article; + .success(function(result){ + article = result; done(); }); }); - self.emails = [ + emails = [ 'default_user@example.com' , 'other_user@example.com' ]; @@ -230,7 +233,7 @@ describe("Article model (RDBMS)", function() { }); it("generates a nodemailer-ready message", function() { - var emailData = this.article.asEmailOptions(this.feed, this.emails); + var emailData = article.asEmailOptions(this.feed, emails); expect(emailData).to.exist; @@ -249,7 +252,7 @@ describe("Article model (RDBMS)", function() { , to: "RSS - my_ feed's _name_ plus shotguns " , bcc: ['default_user@example.com', 'other_user@example.com'] , subject: "article title here: with " - , date: this.data.date + , date: data.date , headers: { "List-ID": this.feed.id + '.rssmtp.firetaco.com' , "List-Unsubscribe": 'http://rssmtp.firetaco.com/feed/' + this.feed.id diff --git a/test/models/Feed.js b/test/models.Feed.js similarity index 96% rename from test/models/Feed.js rename to test/models.Feed.js index 7993162..1ef487d 100644 --- a/test/models/Feed.js +++ b/test/models.Feed.js @@ -1,15 +1,14 @@ -var helper = require('../../support/spec_helper') - , models = require('../../models') - , User = models.User - , Feed = models.Feed - , Article = models.Article - , expect = require('chai').expect - , async = require('async') - , _ = require('lodash') - , request = require('request') - , feedparser = require('../../lib/feedparser') - , moment = require('moment') -; +var helper = require('../support/spec_helper'); +var models = require('../models'); +var User = models.User; +var Feed = models.Feed; +var Article = models.Article; +var expect = require('chai').expect; +var async = require('async'); +var _ = require('lodash'); +var request = require('request'); +var feedparser = require('../lib/feedparser'); +var moment = require('moment'); describe("Feed model (RDBMS)", function() { beforeEach(function() { diff --git a/test/models/User.js b/test/models.User.js similarity index 76% rename from test/models/User.js rename to test/models.User.js index 516aef1..863e842 100644 --- a/test/models/User.js +++ b/test/models.User.js @@ -1,5 +1,5 @@ -var helper = require('../../support/spec_helper') - , models = require('../../models') +var helper = require('../support/spec_helper') + , models = require('../models') , User = models.User , Feed = models.Feed , expect = require('chai').expect @@ -15,21 +15,10 @@ describe("User model (RDBMS)", function() { }); describe("findOrCreateFromOAUTH", function() { - beforeEach(function(done) { - User.create({ - email: 'bob@example.com' - , oauth_provider: 'oauth_provider_here' - , oauth_id: 'oauth_id_here' - }) - .error(done) - .success(function(model){ - this.user = model; - done(); - }.bind(this)); - + beforeEach(function() { this.dummyProfileData = { - provider: 'dummy_oauth_provider' - , id: '3.14159' + provider: 'oauth_provider_here' + , id: 'oauth_id_here' , displayName: 'Bob Foster' , name: { givenName: 'Bob' @@ -48,15 +37,26 @@ describe("User model (RDBMS)", function() { expect(created).to.be.true; expect(user.email).to.equal("bob.foster@example.com"); + expect(user.oauth_id).to.equal('oauth_id_here'); + expect(user.oauth_provider).to.equal('oauth_provider_here'); + done(); }); }); }); describe("when the specified user EXISTS", function() { - beforeEach(function() { - this.dummyProfileData['provider'] = 'oauth_provider_here'; - this.dummyProfileData['id'] = 'oauth_id_here'; + beforeEach(function(done) { + User.create({ + email: 'bob@example.com' + , oauth_provider: this.dummyProfileData.provider + , oauth_id: this.dummyProfileData.id + }) + .error(done) + .success(function(model){ + this.user = model; + done(); + }.bind(this)); }); it("returns the existing user", function(done) { diff --git a/test/server.js b/test/server.js index bfe028b..dbc431e 100644 --- a/test/server.js +++ b/test/server.js @@ -1,11 +1,10 @@ -var helper = require('../support/spec_helper') - , http = require('http') - , models = require('../models') - , Poller = models.poller - , app = require('../app') - , expect = require('chai').expect - , server = require('../server') -; +var helper = require('../support/spec_helper'); +var http = require('http'); +var models = require('../models'); +var agents = require('../agents'); +var app = require('../app'); +var expect = require('chai').expect; +var server = require('../server'); describe("Server", function() { beforeEach(function() { @@ -17,13 +16,13 @@ describe("Server", function() { return fakeServer; }); - var fakeMailer = this.fakeMailer = new models.mailer(); - this.sinon.stub(models, 'mailer', function() { + var fakeMailer = this.fakeMailer = new agents.Mailer(); + this.sinon.stub(agents, 'Mailer', function() { return fakeMailer; }); - this.sinon.stub(Poller.prototype, 'start', function(){}); - this.sinon.spy(models, 'poller'); + this.sinon.stub(agents.Poller.prototype, 'start', function(){}); + this.sinon.spy(agents, 'Poller'); }); it("starts up services", function(done) { @@ -33,11 +32,11 @@ describe("Server", function() { expect(http.createServer).to.have.been.calledWith(app); expect(this.fakeServer.listen).to.have.been.calledWith(3000, '127.0.0.1'); - expect(models.poller).to.have.been.calledWith({ + expect(agents.Poller).to.have.been.calledWith({ FeedClass: models.Feed , mailer: this.fakeMailer }); - expect(Poller.prototype.start).to.have.been.called; + expect(agents.Poller.prototype.start).to.have.been.called; done(); }.bind(this)); diff --git a/views/layout.jade b/views/layout.jade index ff00399..a44fe87 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -1,4 +1,4 @@ -doctype 5 +doctype html html head title RSSMTP