UNPKG

@coboxcoop/space

Version:

a peer-to-peer private and encrypted space

519 lines (444 loc) 16.2 kB
const { describe } = require('tape-plus') const crypto = require('@coboxcoop/crypto') const RAM = require('random-access-memory') const Corestore = require('corestore') const collect = require('collect-stream') const debug = require('@coboxcoop/logger')('@coboxcoop/space') const path = require('path') const fs = require('fs') const mkdirp = require('mkdirp') const randomWords = require('random-words') const sinon = require('sinon') const proxyquire = require('proxyquire') const mount = require('kappa-drive-mount') const { Header } = require('hypertrie/lib/messages') const SpaceAbout = require('@coboxcoop/schemas').encodings.space.about const PeerAbout = require('@coboxcoop/schemas').encodings.peer.about const Space = require('../') const { replicate, tmp, cleanup } = require('./util') describe('@coboxcoop/space: Space', (context) => { let sandbox context.beforeEach((c) => { sandbox = sinon.createSandbox() }) context.afterEach((c) => { sandbox.restore() }) context('constructor() - generates new encryption_key', (assert, next) => { var storage = tmp() var address = crypto.address().toString('hex') var identity = crypto.boxKeyPair() identity.name = randomWords(1).pop() var spy = sandbox.spy(crypto) var ProxySpace = proxyquire('../', { '@coboxcoop/crypto': spy }) var space = ProxySpace(storage, address, identity) assert.ok(space, 'space created') assert.same(space.address, Buffer.from(address, 'hex'), 'has address') assert.same(space.identity.name, identity.name, 'has name') assert.ok(spy.encryptionKey.calledOnce, 'calls crypto.encryptionKey()') assert.ok(spy.encoder.calledOnce, 'uses encryption_key in encoder') cleanup(storage, next) }) context('constructor() - loads encryption_key from path', (assert, next) => { var storage = tmp() var address = crypto.address() var encryptionKey = crypto.encryptionKey() var loadStub = sandbox.stub().returns(encryptionKey) var saveStub = sandbox.stub().returns(true) var ProxySpace = proxyquire('../', { '@coboxcoop/keys': { loadKey: loadStub, saveKey: saveStub }}) var identity = crypto.boxKeyPair() var space = ProxySpace(storage, address, identity) assert.ok(loadStub.calledWith(space.storage), 'key is loaded') assert.ok(saveStub.calledWith(space.storage, 'encryption_key', encryptionKey), 'key is saved') next() }) context('constructor() - saves encryption_key given as argument', (assert, next) => { var storage = tmp() var address = crypto.address().toString('hex') var encryptionKey = crypto.encryptionKey().toString('hex') var space = Space(storage, address, crypto.boxKeyPair(), { encryptionKey }) var key = fs.readFileSync(path.join(space.storage, 'encryption_key')) assert.ok(space, 'space loaded') assert.ok(space.address, address, 'same address') assert.same(key.toString('hex'), encryptionKey.toString('hex'), 'encryption_key stored correctly') cleanup(storage, next) }) // context('reload', (assert, next) => { // var storage = tmp() // var address = crypto.address().toString('hex') // var encryptionKey = crypto.encryptionKey().toString('hex') // var space = Space(storage, address, {}, { // encryptionKey, // corestore: new Corestore(RAM) // }) // write('world', () => { // space.close((err) => { // assert.error(err, 'no error') // var newSpace = Space(storage, address, {}, { // encryptionKey, // corestore: new Corestore(RAM) // }) // newSpace.ready((err) => { // assert.error(err, 'no error') // newSpace.drive.readFile('/hello.txt', (err, data) => { // assert.same(data.toString(), 'world', 'reloads and reads') // done() // }) // }) // }) // }) // function write (data, cb) { // space.ready((err) => { // assert.error(err, 'no error') // space.drive.writeFile('/hello.txt', data, (err) => { // assert.error(err, 'no error') // cb() // }) // }) // } // function done () { // cleanup(storage, next) // } // }) context('destroy()', (assert, next) => { var storage1 = tmp(), storage2 = tmp(), address = crypto.address(), encryptionKey = crypto.encryptionKey() var space1 = Space(storage1, address, { encryptionKey }) space1.ready(() => { space1.destroy((err) => { assert.error(err, 'no error') cleanup([storage1, storage2], next) }) }) }) context('deriveKeyPair()', (assert, next) => { var storage = tmp() var address = crypto.address() var parentKey = crypto.masterKey() var deriveKeyPair = (id, ctxt) => crypto.keyPair(parentKey, id, ctxt) var corestore = new Corestore(RAM, { masterKey: parentKey }) var space = Space(path.join(storage, 'spaces'), address, crypto.boxKeyPair(), { corestore, deriveKeyPair, name: randomWords(1).pop() }) space.ready(() => { var logKp = deriveKeyPair(0, space.address) assert.same(space.log._feed.key, logKp.publicKey, 'Log key derived correctly') assert.same(space.log._feed.secretKey, logKp.secretKey, 'Log secret key derived correctly') var metadataKp = deriveKeyPair(1, space.address) assert.same(space.drive.metadata.key, metadataKp.publicKey, 'Metadata key derived correctly') assert.same(space.drive.metadata.secretKey, metadataKp.secretKey, 'Metadata secret key derived correctly') var contentKp = deriveKeyPair(2, space.address) assert.same(space.drive.content.key, contentKp.publicKey, 'Content key derived correctly') assert.same(space.drive.content.secretKey, contentKp.secretKey, 'Content secret key derived correctly') cleanup(storage, next) }) }) context('replicate() - basic', (assert, next) => { var storage1 = tmp(), storage2 = tmp(), address = crypto.address(), name1 = randomWords(1).pop(), name2 = randomWords(1).pop(), identity = crypto.boxKeyPair(), encryptionKey = crypto.encryptionKey() var space1 = Space(storage1, address, identity, { encryptionKey, name: name1 }) var space2 = Space(storage2, address, identity, { encryptionKey, name: name2 }) var dog = 'dog' var cat = 'cat' space1.ready(() => { space2.ready(() => { space1.writer((err, feed1) => { assert.error(err, 'no error') space2.writer((err, feed2) => { assert.error(err, 'no error') feed1.append(dog, (err, seq) => { assert.error(err, 'no error') feed2.append(cat, (err, seq) => { assert.error(err, 'no error') replicate(space1, space2, (err) => { assert.error(err, 'no error on replicate') var dup2 = space2.feed(feed1.key) assert.ok(dup2, 'gets feed') assert.equal(feed1.key.toString('hex'), dup2.key.toString('hex'), 'feed keys match') assert.equal(dup2.length, 1, 'contains one message') assert.notOk(dup2.secretKey, 'duplicate has no secret key') var dup1 = space1.feed(feed2.key) assert.ok(dup1, 'gets feed') assert.equal(dup1.length, 1, 'contains one message') cleanup([storage1, storage2], next) }) }) }) }) }) }) }) }) context('replicate() - log', (assert, next) => { var storage1 = tmp(), storage2 = tmp(), timestamp = Date.now(), address = crypto.address(), encryptionKey = crypto.encryptionKey(), alice = crypto.boxKeyPair(), bob = crypto.boxKeyPair() var aliceSpace = Space(storage1, address, alice, { encryptionKey, corestore: new Corestore(storage1) }) var bobSpace = Space(storage2, address, bob, { encryptionKey, corestore: new Corestore(storage2) }) var spaceAbout = { type: 'space/about', version: '1.0.0', author: alice.publicKey.toString('hex'), timestamp: Date.now(), content: { name: "Space is the Place" } } var aliceAbout = { type: 'peer/about', version: '1.0.0', author: alice.publicKey.toString('hex'), timestamp: Date.now() + 1, content: { name: 'Alice' } } var bobAbout = { type: 'peer/about', version: '1.0.0', author: bob.publicKey.toString('hex'), timestamp: Date.now() + 2, content: { name: 'Bob' } } publish(aliceSpace, spaceAbout, { valueEncoding: SpaceAbout }, () => { publish(aliceSpace, aliceAbout, { valueEncoding: PeerAbout }, () => { sync(() => { checkBobReplicatedAlice(() => { publish(bobSpace, bobAbout, { valueEncoding: PeerAbout }, () => { sync(() => { checkAliceReplicatedBob() }) }) }) }) }) }) function publish (space, message, opts, cb) { space.ready((err) => { assert.error(err, 'no error') space.log.publish(message, opts, (err, msg) => { assert.error(err, 'no error') cb(err) }) }) } function sync (cb) { aliceSpace.ready(() => bobSpace.ready(() => { replicate(aliceSpace, bobSpace, (err) => { assert.error(err, 'no error on replicate') aliceSpace.ready(() => bobSpace.ready(cb)) }) })) } function checkBobReplicatedAlice (cb) { getMessages(bobSpace, (err, msgs) => { assert.error(err, 'no error') assert.same(msgs.length, 2, 'gets two messages') var values = msgs.map((msg) => msg.value) assert.same(values[0], spaceAbout, 'replicated space/about') assert.same(values[1], aliceAbout, 'replicated peer/about') cb() }) } function checkAliceReplicatedBob () { getMessages(aliceSpace, (err, msgs) => { assert.error(err, 'no error') assert.same(msgs.length, 3, 'gets three messages') var values = msgs.map((msg) => msg.value) assert.same(values[2], bobAbout, 'replicated peer/about') next() }) } function getMessages (space, cb) { var query = [{ $filter: { value: { timestamp: { $gt: 0 } } } }] collect(space.log.read({ query }), cb) } }) context('replicate() - drive', (assert, next) => { var storage1 = tmp(), storage2 = tmp(), address = crypto.address(), encryptionKey = crypto.encryptionKey(), name1 = randomWords(1).pop(), name2 = randomWords(1).pop(), identity = crypto.boxKeyPair() var space1 = Space(storage1, address, identity, { encryptionKey, name: name1 }), space2 = Space(storage2, address, identity, { encryptionKey, name: name2 }) write(space1, 'world', () => { sync(() => { write(space2, 'mundo', () => { sync(() => check(done)) }) }) }) function done () { space1.destroy((err) => { assert.error(err, 'no error') space2.destroy((err) => { assert.error(err, 'no error') cleanup([storage1, storage2], next) }) }) } function write (space, data, cb) { space.ready((err) => { assert.error(err, 'no error') space.drive.writeFile('/hello.txt', data, (err) => { assert.error(err, 'no error') cb() }) }) } function sync (cb) { space1.ready(() => space2.ready(() => { replicate(space1, space2, (err) => { assert.error(err, 'no error') space1.ready(() => space2.ready(cb)) }) })) } function check (cb) { space1.drive.readFile('/hello.txt', (err, data) => { assert.error(err, 'no error') assert.same(data, Buffer.from('mundo'), 'gets latest value') write(space2, 'verden', () => sync(() => { space1.drive.readFile('/hello.txt', (err, data) => { assert.error(err, 'no error') assert.same(data, Buffer.from('verden'), 'gets latest value') cb() }) })) }) } }) context('replicate() - log and drive', (assert, next) => { var storage1 = tmp(), storage2 = tmp(), timestamp = Date.now(), address = crypto.address(), encryptionKey = crypto.encryptionKey(), alice = crypto.boxKeyPair(), bob = crypto.boxKeyPair() var aliceSpace = Space(storage1, address, alice, { encryptionKey, corestore: new Corestore(storage1) }) var bobSpace = Space(storage2, address, bob, { encryptionKey, corestore: new Corestore(storage2) }) var spaceAbout = { type: 'space/about', version: '1.0.0', author: alice.publicKey.toString('hex'), timestamp: Date.now(), content: { name: "Space is the Place" } } var aliceAbout = { type: 'peer/about', version: '1.0.0', author: alice.publicKey.toString('hex'), timestamp: Date.now() + 1, content: { name: 'Alice' } } var bobAbout = { type: 'peer/about', version: '1.0.0', author: bob.publicKey.toString('hex'), timestamp: Date.now() + 2, content: { name: 'Bob' } } publish(aliceSpace, spaceAbout, { valueEncoding: SpaceAbout }, () => { publish(aliceSpace, aliceAbout, { valueEncoding: PeerAbout }, () => { write(aliceSpace, 'hello bob', () => { sync(() => { checkBobReplicatedAlice(() => { publish(bobSpace, bobAbout, { valueEncoding: PeerAbout }, () => { write(bobSpace, 'hi alice', () => { sync(() => { checkAliceReplicatedBob() }) }) }) }) }) }) }) }) function publish (space, message, opts, cb) { space.ready((err) => { assert.error(err, 'no error') space.log.publish(message, opts, (err, msg) => { assert.error(err, 'no error') cb(err) }) }) } function write (space, data, cb) { space.ready((err) => { assert.error(err, 'no error') space.drive.writeFile('/hello.txt', data, (err) => { assert.error(err, 'no error') cb() }) }) } function sync (cb) { aliceSpace.ready(() => bobSpace.ready(() => { replicate(aliceSpace, bobSpace, (err) => { assert.error(err, 'no error on replicate') aliceSpace.ready(() => bobSpace.ready(cb)) }) })) } function checkBobReplicatedAlice (cb) { getMessages(bobSpace, (err, msgs) => { assert.error(err, 'no error') assert.same(msgs.length, 2, 'gets two messages') var values = msgs.map((msg) => msg.value) assert.same(values[0], spaceAbout, 'replicated space/about') assert.same(values[1], aliceAbout, 'replicated peer/about') bobSpace.drive.readFile('/hello.txt', (err, data) => { assert.error(err, 'no error') assert.same(data.toString(), 'hello bob', 'replicated drive') cb() }) }) } function checkAliceReplicatedBob () { getMessages(aliceSpace, (err, msgs) => { assert.error(err, 'no error') assert.same(msgs.length, 3, 'gets three messages') var values = msgs.map((msg) => msg.value) assert.same(values[2], bobAbout, 'replicated peer/about') aliceSpace.drive.readFile('/hello.txt', (err, data) => { assert.error(err, 'no error') assert.same(data.toString(), 'hi alice', 'replicated drive') next() }) }) } function getMessages (space, cb) { var query = [{ $filter: { value: { timestamp: { $gt: 0 } } } }] collect(space.log.read({ query }), cb) } }) })