From ce2b24d83dbeeab4d21d2fe59b1184b6a88a8a58 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 25 May 2016 16:13:49 +0200 Subject: [PATCH] Implement MongoDB client --- .gitignore | 3 + .jscsrc | 4 + .jshintrc | 25 +++++++ .travis.yml | 17 +++++ Gruntfile.js | 43 +++++++++++ README.md | 4 +- package.json | 28 +++++++ src/dao/mongo.js | 124 +++++++++++++++++++++++++++++++ test/integration/mongo-test.js | 131 +++++++++++++++++++++++++++++++++ 9 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 .jscsrc create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 Gruntfile.js create mode 100644 package.json create mode 100644 src/dao/mongo.js create mode 100644 test/integration/mongo-test.js diff --git a/.gitignore b/.gitignore index e920c16..cf01bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.DS_Store +credentials.json + # Logs logs *.log diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..7d15b2b --- /dev/null +++ b/.jscsrc @@ -0,0 +1,4 @@ +{ + "disallowTrailingWhitespace": true, + "validateIndentation": 2 +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..f4d30cf --- /dev/null +++ b/.jshintrc @@ -0,0 +1,25 @@ +{ + "strict": true, + "node": true, + "nonew": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "newcap": true, + "regexp": true, + "evil": true, + "eqnull": true, + "expr": true, + "undef": true, + "unused": true, + "esnext": true, + + "globals": { + "describe" : true, + "it" : true, + "before" : true, + "beforeEach" : true, + "after" : true, + "afterEach" : true + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a6d2dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +language: node_js +node_js: + - "4" + - "5" + - "6" +before_script: + - npm install -g grunt-cli + - sleep 15 + - mongo test_db --eval 'db.addUser("travis", "test");' +notifications: + email: + - build@mailvelope.com +services: + - mongodb +env: + - MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..d01bcd7 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,43 @@ +'use strict'; + +module.exports = function(grunt) { + + grunt.initConfig({ + jshint: { + all: ['*.js', 'src/**/*.js', 'test/**/*.js'], + options: { + jshintrc: '.jshintrc' + } + }, + + jscs: { + src: ['*.js', 'src/**/*.js', 'test/**/*.js'], + options: { + config: ".jscsrc", + esnext: true, // If you use ES6 http://jscs.info/overview.html#esnext + verbose: true, // If you need output with rule names http://jscs.info/overview.html#verbose + } + }, + + mochaTest: { + test: { + options: { + reporter: 'spec' + }, + src: [ + 'test/unit/*.js', + 'test/integration/*.js', + ] + } + } + }); + + // Load the plugin(s) + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-jscs'); + grunt.loadNpmTasks('grunt-mocha-test'); + + // Default task(s). + grunt.registerTask('test', ['jshint', 'jscs', 'mochaTest']); + +}; \ No newline at end of file diff --git a/README.md b/README.md index ea2e1a5..710ac7b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# keyserver +Mailvelope Keyserver [![Build Status](https://travis-ci.org/mailvelope/keyserver.svg?branch=master)](https://travis-ci.org/mailvelope/keyserver) +============== + A simple OpenPGP public key server diff --git a/package.json b/package.json new file mode 100644 index 0000000..f4887f0 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "mailvelope-keyserver", + "version": "0.0.1", + "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/mailvelope/keyserver.git" + }, + "engines": { + "node": ">=4" + }, + "scripts": { + "test": "grunt test" + }, + "dependencies": { + "mongodb": "^2.1.20" + }, + "devDependencies": { + "chai": "^3.5.0", + "co-mocha": "^1.1.2", + "grunt": "^1.0.1", + "grunt-contrib-jshint": "^1.0.0", + "grunt-jscs": "^2.8.0", + "grunt-mocha-test": "^0.12.7", + "mocha": "^2.5.3", + "sinon": "^1.17.4" + } +} diff --git a/src/dao/mongo.js b/src/dao/mongo.js new file mode 100644 index 0000000..0b9b673 --- /dev/null +++ b/src/dao/mongo.js @@ -0,0 +1,124 @@ +/** + * Mailvelope - secure email with OpenPGP encryption for Webmail + * Copyright (C) 2016 Mailvelope GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License version 3 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict'; + +const MongoClient = require('mongodb').MongoClient; + +/** + * A simple wrapper around the official MongoDB client. + */ +class Mongo { + + /** + * Create an instance of the MongoDB client. + * @param {String} options.uri The mongodb uri + * @param {String} options.user The databse user + * @param {String} options.password The database user's password + * @param {String} options.type (optional) The default collection type to use e.g. 'publickey' + * @return {undefined} + */ + constructor(options) { + this._uri = 'mongodb://' + options.user + ':' + options.password + '@' + options.uri; + this._type = options.type; + } + + /** + * Initializes the database client by connecting to the MongoDB. + * @return {undefined} + */ + *connect() { + this._db = yield MongoClient.connect(this._uri); + } + + /** + * Cleanup by closing the connection to the database. + * @return {undefined} + */ + disconnect() { + return this._db.close(); + } + + /** + * Inserts a single document. + * @param {Object} document Inserts a single documents + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Object} The operation result + */ + create(document, type) { + let col = this._db.collection(type || this._type); + return col.insertOne(document); + } + + /** + * Update a single document. + * @param {Object} query The query e.g. { _id:'0' } + * @param {Object} diff The attributes to change/set e.g. { foo:'bar' } + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Object} The operation result + */ + update(query, diff, type) { + let col = this._db.collection(type || this._type); + return col.updateOne(query, { $set:diff }); + } + + /** + * Read a single document. + * @param {Object} query The query e.g. { _id:'0' } + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Object} The document object + */ + get(query, type) { + let col = this._db.collection(type || this._type); + return col.findOne(query); + } + + /** + * Read multiple documents at once. + * @param {Object} query The query e.g. { foo:'bar' } + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Array} An array of document objects + */ + list(query, type) { + let col = this._db.collection(type || this._type); + return col.find(query).toArray(); + } + + /** + * Delete a single document. + * @param {Object} query The query e.g. { _id:'0' } + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Object} The document object + */ + remove(query, type) { + let col = this._db.collection(type || this._type); + return col.deleteOne(query); + } + + /** + * Clear all documents of a collection. + * @param {String} type (optional) The collection to use e.g. 'publickey' + * @return {Object} The operation result + */ + clear(type) { + let col = this._db.collection(type || this._type); + return col.deleteMany({}); + } + +} + +module.exports = Mongo; \ No newline at end of file diff --git a/test/integration/mongo-test.js b/test/integration/mongo-test.js new file mode 100644 index 0000000..2542e7c --- /dev/null +++ b/test/integration/mongo-test.js @@ -0,0 +1,131 @@ +'use strict'; + +require('co-mocha')(require('mocha')); // monkey patch mocha for generators + +const Mongo = require('../../src/dao/mongo'), + expect = require('chai').expect, + fs = require('fs'); + +describe('Mongo Integration Tests', function() { + this.timeout(20000); + + const defaultType = 'apple'; + const secondaryType = 'orange'; + let mongo; + + before(function *() { + let credentials; + try { + credentials = JSON.parse(fs.readFileSync(__dirname + '/../../credentials.json')); + } catch(e) {} + mongo = new Mongo({ + uri: process.env.MONGO_URI || credentials.mongoUri, + user: process.env.MONGO_USER || credentials.mongoUser, + password: process.env.MONGO_PASS || credentials.mongoPass, + type: defaultType + }); + yield mongo.connect(); + }); + + beforeEach(function *() { + yield mongo.clear(); + yield mongo.clear(secondaryType); + }); + + afterEach(function() {}); + + after(function *() { + yield mongo.clear(); + yield mongo.clear(secondaryType); + yield mongo.disconnect(); + }); + + describe("create", function() { + it('should insert a document', function *() { + let r = yield mongo.create({ _id:'0' }); + expect(r.insertedCount).to.equal(1); + }); + + it('should insert a document with a type', function *() { + let r = yield mongo.create({ _id:'0' }); + expect(r.insertedCount).to.equal(1); + r = yield mongo.create({ _id:'0' }, secondaryType); + expect(r.insertedCount).to.equal(1); + }); + + it('should fail if two with the same ID are inserted', function *() { + let r = yield mongo.create({ _id:'0' }); + expect(r.insertedCount).to.equal(1); + try { + r = yield mongo.create({ _id:'0' }); + } catch(e) { + expect(e.message).to.match(/duplicate/); + } + }); + }); + + describe("update", function() { + it('should update a document', function *() { + let r = yield mongo.create({ _id:'0' }); + r = yield mongo.update({ _id:'0' }, { foo:'bar' }); + expect(r.modifiedCount).to.equal(1); + r = yield mongo.get({ _id:'0' }); + expect(r.foo).to.equal('bar'); + }); + + it('should update a document with a type', function *() { + let r = yield mongo.create({ _id:'0' }, secondaryType); + r = yield mongo.update({ _id:'0' }, { foo:'bar' }, secondaryType); + expect(r.modifiedCount).to.equal(1); + r = yield mongo.get({ _id:'0' }, secondaryType); + expect(r.foo).to.equal('bar'); + }); + }); + + describe("get", function() { + it('should get a document', function *() { + let r = yield mongo.create({ _id:'0' }); + r = yield mongo.get({ _id:'0' }); + expect(r).to.exist; + }); + + it('should get a document with a type', function *() { + let r = yield mongo.create({ _id:'0' }, secondaryType); + r = yield mongo.get({ _id:'0' }, secondaryType); + expect(r).to.exist; + }); + }); + + describe("list", function() { + it('should list documents', function *() { + let r = yield mongo.create({ _id:'0', foo:'bar' }); + r = yield mongo.create({ _id:'1', foo:'bar' }); + r = yield mongo.list({ foo:'bar' }); + expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }]); + }); + + it('should list documents with a type', function *() { + let r = yield mongo.create({ _id:'0', foo:'bar' }, secondaryType); + r = yield mongo.create({ _id:'1', foo:'bar' }, secondaryType); + r = yield mongo.list({ foo:'bar' }, secondaryType); + expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }]); + }); + }); + + describe("remove", function() { + it('should remove a document', function *() { + let r = yield mongo.create({ _id:'0' }); + r = yield mongo.remove({ _id:'0' }); + r = yield mongo.get({ _id:'0' }); + expect(r).to.not.exist; + }); + + it('should remove a document with a type', function *() { + let r = yield mongo.create({ _id:'0' }, secondaryType); + r = yield mongo.remove({ _id:'0' }, secondaryType); + r = yield mongo.get({ _id:'0' }, secondaryType); + expect(r).to.not.exist; + }); + }); + +}); \ No newline at end of file