Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
johanbrook committed May 21, 2016
0 parents commit 2ed7322
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "lookback/meteor",

"globals": {
"PublicationCollector": true
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
npm-debug.log
.DS_Store
.atomignore
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Publication Collector

This package makes testing publications in Meteor easier and nicer.

Instead of resorting to exporting or exposing your publication functions for doing testing, this package lets you "subscribe" to a given publication and assert its returned results.

## Installation

```
meteor add johanbrook:publication-collector
```

## Usage

```js
// In a typical BDD style test suite:

describe('Publication', function() {
if('should publish 10 documents', function() {
// Pass user context in constructor.
const collector = new PublicationCollector({userId: Random.id()});

// Collect documents from a subscription with 'collect(name, [arguments...], [callback])'
collector.collect('publicationName', 'someArgument', (collections) => {
assert.equal(collections.myCollection.length, 10);
});
});
});
```

An instance of `PublicationCollector` also is an `EventEmitter`, and emits a `ready` event when the publication is marked as ready.

## Tests

Run tests once with

```
npm test
```

Run tests in watch mode (in console) with

```
npm run test:dev
```

## History

This project was originally a part of MDG's [Todos](https://github.com/meteor/todos) example Meteor app, but later extracted as a separate test package.

Based on https://github.com/stubailo/meteor-rest/blob/devel/packages/rest/http-subscription.js.

## To do

- [ ] Make tests pass.
- [ ] More docs.
- [ ] Support Promises.
43 changes: 43 additions & 0 deletions package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable prefer-arrow-callback */

Package.describe({
name: 'johanbrook:publication-collector',
version: '1.0.0',
summary: 'Test a Meteor publication by collecting its output.',
documentation: 'README.md',
git: 'https://github.com/johanbrook/publication-collector.git',
debugOnly: true
});

Package.onUse(function(api) {
api.versionsFrom('1.2.0.2');

api.use([
'ecmascript',
'underscore',
'check'
], 'server');

api.addFiles('publication-collector.js', 'server');

api.export('PublicationCollector', 'server');
});

Package.onTest(function(api) {
api.use([
'ecmascript',
'mongo',
'random',
'dispatch:mocha',
'practicalmeteor:sinon',
'practicalmeteor:chai@2.1.0_1',
'underscore'
], 'server');

api.use('johanbrook:publication-collector');

api.addFiles([
'tests/publications.js',
'tests/publication-collector.test.js'
], 'server');
});
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "publication-collector",
"version": "1.0.0",
"description": "Test a Meteor publication by collecting its output.",
"scripts": {
"test": "SERVER_TEST_REPORTER='dot' meteor test-packages --once --driver-package dispatch:mocha ./",
"test:dev": "TEST_WATCH=1 meteor test-packages --driver-package dispatch:mocha ./",
"lint": "eslint --format=node_modules/eslint-formatter-pretty ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/johanbrook/publication-collector.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/johanbrook/publication-collector/issues"
},
"devDependencies": {
"babel-preset-es2015": "^6.3.13",
"eslint": "^2.2.0",
"eslint-config-lookback": "lookback/eslint-config-lookback",
"eslint-formatter-pretty": "^0.2.0"
}
}
109 changes: 109 additions & 0 deletions publication-collector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const EventEmitter = Npm.require('events').EventEmitter;

/*
This file describes something like Subscription in
meteor/meteor/packages/ddp/livedata_server.js, but instead of sending
over a socket it just collects data.
*/

PublicationCollector = function(context = {}) {
check(context.userId, Match.Optional(String));

// Object where the keys are collection names, and then the keys are _ids
this.responseData = {};

this.userId = context.userId;
};

// So that we can listen to ready event in a reasonable way
Meteor._inherits(PublicationCollector, EventEmitter);

_.extend(PublicationCollector.prototype, {
collect(name, ...args) {
if (_.isFunction(args[args.length - 1])) {
this.on('ready', args.pop());
}

const handler = Meteor.server.publish_handlers[name];
const result = handler.call(this, ...args);

// TODO -- we should check that result has _publishCursor? What does _runHandler do?
if (result) {
// array-ize
[].concat(result).forEach(cur => cur._publishCursor(this));
this.ready();
}
},

added(collection, id, fields) {
check(collection, String);
check(id, String);

this._ensureCollectionInRes(collection);

// Make sure to ignore the _id in fields
const addedDocument = _.extend({_id: id}, _.omit(fields, '_id'));
this.responseData[collection][id] = addedDocument;
},

changed(collection, id, fields) {
check(collection, String);
check(id, String);

this._ensureCollectionInRes(collection);

const existingDocument = this.responseData[collection][id];
const fieldsNoId = _.omit(fields, '_id');
_.extend(existingDocument, fieldsNoId);

// Delete all keys that were undefined in fields (except _id)
_.forEach(fields, (value, key) => {
if (value === undefined) {
delete existingDocument[key];
}
});
},

removed(collection, id) {
check(collection, String);
check(id, String);

this._ensureCollectionInRes(collection);

delete this.responseData[collection][id];

if (_.isEmpty(this.responseData[collection])) {
delete this.responseData[collection];
}
},

ready() {
this.emit('ready', this._generateResponse());
},

onStop() {
// no-op
},

stop() {
// no-op
},

error(error) {
throw error;
},

_ensureCollectionInRes(collection) {
this.responseData[collection] = this.responseData[collection] || {};
},

_generateResponse() {
const output = {};

_.forEach(this.responseData, (documents, collectionName) => {
output[collectionName] = _.values(documents);
});

return output;
}
});
113 changes: 113 additions & 0 deletions tests/publication-collector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* eslint-env mocha */
/* global Documents, spies */

const { assert } = Package['practicalmeteor:chai'];

PublicationCollector = Package['johanbrook:publication-collector'].PublicationCollector;

describe('PublicationCollector', () => {

beforeEach(() => {
Documents.remove({});
_.times(10, () => Documents.insert({foo: 'bar'}));
});

it('should be able to instantiate', () => {
const instance = new PublicationCollector();
assert.ok(instance);
});

describe('Collect', () => {

it('should collect documents from a publication', () => {
const collector = new PublicationCollector();

collector.collect('publication', collections => {
assert.ok(collections.documents);
assert.equal(collections.documents.length, 10, 'collects 10 documents');
});
});

it('should pass the correct scope to the publication', () => {
const collector = new PublicationCollector({userId: 'foo'});

collector.collect('publicationWithUser', collections => {
assert.ok(collections.documents);
assert.equal(collections.documents.length, 10, 'collects 10 documents');
});
});

it('should emit ready event', () => {
const collector = new PublicationCollector();

collector.on('ready', spies.create('ready'));

collector.collect('publication');
assert.ok(spies.ready.calledOnce, 'ready was called');
});

it('should pass arguments to publication', (done) => {
Meteor.publish('publicationWithArgs', function(arg1, arg2) {
assert.equal(arg1, 'foo');
assert.equal(arg2, 'bar');
this.ready();
done();
});

const collector = new PublicationCollector();

collector.collect('publicationWithArgs', 'foo', 'bar');
});
});

describe('Added', () => {

it('should add a document to the local data store', () => {
const collector = new PublicationCollector();

const id = Random.id();
const doc = {_id: id, foo: 'bar'};
collector.added('documents', doc._id, doc);

assert.deepEqual(collector.responseData.documents[id], doc);
});
});

describe('Removed', () => {

it('should remove a document to the local data store', () => {
const collector = new PublicationCollector();

const doc = Documents.findOne();
collector.collect('publication');
collector.removed('documents', doc._id);

assert.notOk(collector.responseData.documents[doc._id]);
assert.equal(Object.keys(collector.responseData.documents).length, 9);
});
});

describe('Error', () => {

it('should throw error when .error() is called in publication', () => {
// We're not passing a user context here to trigger error.
const collector = new PublicationCollector();

assert.throws(() => {
collector.collect('publicationError');
}, Meteor.Error, /Not authorized/);
});
});

describe('_generateResponse', () => {

it('should generate a response with collection names as keys', () => {
const collector = new PublicationCollector();

collector.collect('publication', collections => {
assert.equal(Object.keys(collections).length, 1);
assert.equal(Object.keys(collections)[0], 'documents');
});
});
});
});
26 changes: 26 additions & 0 deletions tests/publications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* global Documents: true */
/* eslint-disable prefer-arrow-callback */

Documents = new Mongo.Collection('documents');

Meteor.publish('publication', function() {
return Documents.find();
});

Meteor.publish('publicationWithUser', function() {

if (!this.userId || this.userId !== 'foo') {
return this.ready();
}

return Documents.find();
});

Meteor.publish('publicationError', function() {

if (!this.userId) {
this.error(new Meteor.Error('not-authorized', 'Not authorized'));
}

return Documents.find();
});

0 comments on commit 2ed7322

Please sign in to comment.