diff --git a/package.json b/package.json index 54e9d491d9..3bdaa3b57a 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "ncp": "^2.0.0", "qs": "^6.5.2", "rimraf": "^2.6.2", + "sinon": "^7.1.1", "stream-to-promise": "^2.2.0" }, "dependencies": { @@ -87,6 +88,7 @@ "byteman": "^1.3.5", "cid-tool": "~0.1.0", "cids": "~0.5.5", + "datastore-core": "~0.6.0", "debug": "^4.1.0", "deep-extend": "~0.6.0", "err-code": "^1.1.2", diff --git a/src/core/components/init.js b/src/core/components/init.js index 3265c496fc..3473516b9f 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -8,6 +8,9 @@ const defaultsDeep = require('@nodeutils/defaults-deep') const defaultConfig = require('../runtime/config-nodejs.js') const Keychain = require('libp2p-keychain') +const IPNS = require('../ipns') +const OfflineDatastore = require('../ipns/routing/offline-datastore') + const addDefaultAssets = require('./init-assets') module.exports = function init (self) { @@ -105,6 +108,14 @@ module.exports = function init (self) { cb(null, true) } }, + // Setup the offline routing for IPNS. + // This is primarily used for offline ipns modifications, such as the initializeKeyspace feature. + (_, cb) => { + const offlineDatastore = new OfflineDatastore(self._repo) + + self._ipns = new IPNS(offlineDatastore, self._repo, self._peerInfo, self._keychain, self._options) + cb(null, true) + }, // add empty unixfs dir object (go-ipfs assumes this exists) (_, cb) => { if (opts.emptyRepo) { diff --git a/src/core/components/name.js b/src/core/components/name.js index f53878f84e..8acb5c89c2 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -128,7 +128,7 @@ module.exports = function name (self) { const nocache = options.nocache && options.nocache.toString() === 'true' const recursive = options.recursive && options.recursive.toString() === 'true' - const local = true // TODO ROUTING - use self._options.local + const local = self._options.local if (!self.isOnline() && !local) { const errMsg = utils.OFFLINE_ERROR @@ -157,11 +157,10 @@ module.exports = function name (self) { const resolveOptions = { nocache, - recursive, - local + recursive } - self._ipns.resolve(name, self._peerInfo.id, resolveOptions, callback) + self._ipns.resolve(name, resolveOptions, callback) }) } } diff --git a/src/core/components/start.js b/src/core/components/start.js index 7b4df7cb61..518d613b0e 100644 --- a/src/core/components/start.js +++ b/src/core/components/start.js @@ -4,6 +4,10 @@ const series = require('async/series') const Bitswap = require('ipfs-bitswap') const setImmediate = require('async/setImmediate') const promisify = require('promisify-es6') +const { TieredDatastore } = require('datastore-core') + +const IPNS = require('../ipns') +const OfflineDatastore = require('../ipns/routing/offline-datastore') module.exports = (self) => { return promisify((callback) => { @@ -34,6 +38,20 @@ module.exports = (self) => { }, (cb) => self.libp2p.start(cb), (cb) => { + // Setup online routing for IPNS with a tiered routing composed by a DHT and a Pubsub router (if properly enabled) + const ipnsStores = [] + + // TODO Add IPNS pubsub if enabled + + // NOTE: IPNS routing is being replaced by the local repo datastore while the IPNS over DHT is not ready + // When DHT is added, if local option enabled, should receive offlineDatastore as well + const offlineDatastore = new OfflineDatastore(self._repo) + ipnsStores.push(offlineDatastore) + + // Create ipns routing with a set of datastores + const routing = new TieredDatastore(ipnsStores) + self._ipns = new IPNS(routing, self._repo, self._peerInfo, self._keychain, self._options) + self._bitswap = new Bitswap( self._libp2pNode, self._repo.blocks, diff --git a/src/core/index.js b/src/core/index.js index 034a8a494f..93ff3431d8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -18,7 +18,7 @@ const EventEmitter = require('events') const config = require('./config') const boot = require('./boot') const components = require('./components') -const IPNS = require('./ipns') + // replaced by repo-browser when running in the browser const defaultRepo = require('./runtime/repo-nodejs') const preload = require('./preload') @@ -126,7 +126,7 @@ class IPFS extends EventEmitter { }) this._preload = preload(this) this._mfsPreload = mfsPreload(this) - this._ipns = new IPNS(null, this) + this._ipns = undefined // IPFS Core exposed components // - for booting up a node diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 8bfc7cd718..06b43ca330 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -4,6 +4,7 @@ const { createFromPrivKey } = require('peer-id') const series = require('async/series') const Receptacle = require('receptacle') +const errcode = require('err-code') const debug = require('debug') const log = debug('jsipfs:ipns') log.error = debug('jsipfs:ipns:error') @@ -16,11 +17,12 @@ const path = require('./path') const defaultRecordTtl = 60 * 1000 class IPNS { - constructor (routing, ipfs) { - this.publisher = new IpnsPublisher(routing, ipfs._repo) - this.republisher = new IpnsRepublisher(this.publisher, ipfs) - this.resolver = new IpnsResolver(routing, ipfs._repo) + constructor (routing, repo, peerInfo, keychain, options) { + this.publisher = new IpnsPublisher(routing, repo) + this.republisher = new IpnsRepublisher(this.publisher, repo, peerInfo, keychain, options) + this.resolver = new IpnsResolver(routing) this.cache = new Receptacle({ max: 1000 }) // Create an LRU cache with max 1000 items + this.routing = routing } // Publish @@ -53,7 +55,21 @@ class IPNS { } // Resolve - resolve (name, peerId, options, callback) { + resolve (name, options, callback) { + if (typeof name !== 'string') { + const errMsg = `name received is not valid` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_NAME')) + } + + if (typeof options === 'function') { + callback = options + options = {} + } + + options = options || {} + // If recursive, we should not try to get the cached value if (!options.nocache && !options.recursive) { // Try to get the record from cache @@ -67,7 +83,7 @@ class IPNS { } } - this.resolver.resolve(name, peerId, options, (err, result) => { + this.resolver.resolve(name, options, (err, result) => { if (err) { log.error(err) return callback(err) diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 1bb05a8f90..9611f56165 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -13,13 +13,7 @@ const resolvePath = (ipfsNode, name, callback) => { if (isIPFS.ipnsPath(name)) { log(`resolve ipns path ${name}`) - const local = true // TODO ROUTING - use self._options.local - - const options = { - local: local - } - - return ipfsNode._ipns.resolve(name, ipfsNode._peerInfo.id, options, callback) + return ipfsNode._ipns.resolve(name, callback) } // ipfs path diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index cce582f699..9e8fcdf3f3 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -32,7 +32,7 @@ class IpnsPublisher { PeerId.createFromPrivKey(privKey.bytes, (err, peerId) => { if (err) { - callback(err) + return callback(err) } this._updateOrCreateRecord(privKey, value, lifetime, peerId, (err, record) => { @@ -67,17 +67,17 @@ class IpnsPublisher { let keys try { - keys = ipns.getIdKeys(peerId.id) + keys = ipns.getIdKeys(peerId.toBytes()) } catch (err) { log.error(err) return callback(err) } series([ - (cb) => this._publishEntry(keys.ipnsKey, embedPublicKeyRecord || record, peerId, cb), + (cb) => this._publishEntry(keys.routingKey, embedPublicKeyRecord || record, peerId, cb), // Publish the public key if a public key cannot be extracted from the ID // We will be able to deprecate this part in the future, since the public keys will be only in the peerId - (cb) => embedPublicKeyRecord ? this._publishPublicKey(keys.pkKey, publicKey, peerId, cb) : cb() + (cb) => embedPublicKeyRecord ? this._publishPublicKey(keys.routingPubKey, publicKey, peerId, cb) : cb() ], (err) => { if (err) { log.error(err) @@ -108,13 +108,13 @@ class IpnsPublisher { return callback(err) } - // TODO Routing - this should be replaced by a put to the DHT - this._repo.datastore.put(key, rec.serialize(), (err, res) => { + // Add record to routing (buffer key) + this._routing.put(key.toBuffer(), rec.serialize(), (err, res) => { if (err) { const errMsg = `ipns record for ${key.toString()} could not be stored in the routing` log.error(errMsg) - return callback(errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE')) + return callback(errcode(new Error(errMsg), 'ERR_PUTTING_TO_ROUTING')) } log(`ipns record for ${key.toString()} was stored in the routing`) @@ -146,13 +146,13 @@ class IpnsPublisher { return callback(err) } - // TODO Routing - this should be replaced by a put to the DHT - this._repo.datastore.put(key, rec.serialize(), (err, res) => { + // Add public key to routing (buffer key) + this._routing.put(key.toBuffer(), rec.serialize(), (err, res) => { if (err) { const errMsg = `public key for ${key.toString()} could not be stored in the routing` log.error(errMsg) - return callback(errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE')) + return callback(errcode(new Error(errMsg), 'ERR_PUTTING_TO_ROUTING')) } log(`public key for ${key.toString()} was stored in the routing`) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 9ef02158d6..150cb313d7 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -18,10 +18,12 @@ const defaultBroadcastInterval = 4 * hour const defaultRecordLifetime = 24 * hour class IpnsRepublisher { - constructor (publisher, ipfs) { + constructor (publisher, repo, peerInfo, keychain, options) { this._publisher = publisher - this._ipfs = ipfs - this._repo = ipfs._repo + this._repo = repo + this._peerInfo = peerInfo + this._keychain = keychain + this._options = options this._republishHandle = null } @@ -62,8 +64,8 @@ class IpnsRepublisher { } } - const { privKey } = this._ipfs._peerInfo.id - const { pass } = this._ipfs._options + const { privKey } = this._peerInfo.id + const { pass } = this._options republishHandle.runPeriodically((done) => { this._republishEntries(privKey, pass, () => done(defaultBroadcastInterval)) @@ -98,8 +100,8 @@ class IpnsRepublisher { } // keychain needs pass to get the cryptographic keys - if (this._ipfs._keychain && Boolean(pass)) { - this._ipfs._keychain.listKeys((err, list) => { + if (pass) { + this._keychain.listKeys((err, list) => { if (err) { log.error(err) return @@ -107,7 +109,7 @@ class IpnsRepublisher { each(list, (key, cb) => { waterfall([ - (cb) => this._ipfs._keychain.exportKey(key.name, pass, cb), + (cb) => this._keychain.exportKey(key.name, pass, cb), (pem, cb) => crypto.keys.import(pem, pass, cb) ], (err, privKey) => { if (err) { diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index b70a1bf938..f9fb81ccd5 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -1,8 +1,8 @@ 'use strict' const ipns = require('ipns') -const { fromB58String } = require('multihashes') const Record = require('libp2p-record').Record +const PeerId = require('peer-id') const errcode = require('err-code') const debug = require('debug') @@ -12,13 +12,11 @@ log.error = debug('jsipfs:ipns:resolver:error') const defaultMaximumRecursiveDepth = 32 class IpnsResolver { - constructor (routing, repo) { + constructor (routing) { this._routing = routing - this._repo = repo - this._resolver = undefined // TODO Routing - add Router resolver } - resolve (name, peerId, options, callback) { + resolve (name, options, callback) { if (typeof options === 'function') { callback = options options = {} @@ -33,7 +31,6 @@ class IpnsResolver { options = options || {} const recursive = options.recursive && options.recursive.toString() === 'true' - const local = !(options.local === false) const nameSegments = name.split('/') @@ -53,20 +50,7 @@ class IpnsResolver { depth = defaultMaximumRecursiveDepth } - // Get the intended resoulver function - // TODO Routing - set default resolverFn - - let resolverFn - - if (local) { - resolverFn = this._resolveLocal - } - - if (!resolverFn) { - return callback(new Error('not implemented yet')) - } - - this.resolver(key, depth, peerId, resolverFn, (err, res) => { + this.resolver(key, depth, (err, res) => { if (err) { return callback(err) } @@ -77,10 +61,7 @@ class IpnsResolver { } // Recursive resolver according to the specified depth - resolver (name, depth, peerId, resolverFn, callback) { - // bind resolver function - this._resolver = resolverFn - + resolver (name, depth, callback) { // Exceeded recursive maximum depth if (depth === 0) { const errMsg = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` @@ -89,7 +70,7 @@ class IpnsResolver { return callback(errcode(new Error(errMsg), 'ERR_RESOLVE_RECURSION_LIMIT')) } - this._resolver(name, peerId, (err, res) => { + this._resolveName(name, (err, res) => { if (err) { return callback(err) } @@ -102,32 +83,51 @@ class IpnsResolver { } // continue recursively until depth equals 0 - this.resolver(nameSegments[2], depth - 1, peerId, resolverFn, callback) + this.resolver(nameSegments[2], depth - 1, callback) }) } - // resolve ipns entries locally using the datastore - _resolveLocal (name, peerId, callback) { - const { ipnsKey } = ipns.getIdKeys(fromB58String(name)) + // resolve ipns entries from the provided routing + _resolveName (name, callback) { + let peerId - this._repo.datastore.get(ipnsKey, (err, dsVal) => { + try { + peerId = PeerId.createFromB58String(name) + } catch (err) { + return callback(err) + } + + const { routingKey } = ipns.getIdKeys(peerId.toBytes()) + + // TODO DHT - get public key from routing? + // https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go#L70 + // https://github.com/libp2p/go-libp2p-routing/blob/master/routing.go#L99 + + this._routing.get(routingKey.toBuffer(), (err, res) => { if (err) { - const errMsg = `local record requested was not found for ${name} (${ipnsKey})` + if (err.code !== 'ERR_NOT_FOUND') { + const errMsg = `unexpected error getting the ipns record ${peerId.id}` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_UNEXPECTED_ERROR_GETTING_RECORD')) + } + const errMsg = `record requested was not found for ${name} (${routingKey}) in the network` log.error(errMsg) - return callback(errcode(new Error(errMsg), 'ERR_NO_LOCAL_RECORD_FOUND')) + return callback(errcode(new Error(errMsg), 'ERR_NO_RECORD_FOUND')) } - if (!Buffer.isBuffer(dsVal)) { + let ipnsEntry + try { + const record = Record.deserialize(res) + ipnsEntry = ipns.unmarshal(record.value) + } catch (err) { const errMsg = `found ipns record that we couldn't convert to a value` log.error(errMsg) return callback(errcode(new Error(errMsg), 'ERR_INVALID_RECORD_RECEIVED')) } - const record = Record.deserialize(dsVal) - const ipnsEntry = ipns.unmarshal(record.value) - ipns.extractPublicKey(peerId, ipnsEntry, (err, pubKey) => { if (err) { return callback(err) diff --git a/src/core/ipns/routing/offline-datastore.js b/src/core/ipns/routing/offline-datastore.js new file mode 100644 index 0000000000..26de52528c --- /dev/null +++ b/src/core/ipns/routing/offline-datastore.js @@ -0,0 +1,88 @@ +'use strict' + +const { Key } = require('interface-datastore') +const { encodeBase32 } = require('./utils') + +const errcode = require('err-code') +const debug = require('debug') +const log = debug('jsipfs:ipns:offline-datastore') +log.error = debug('jsipfs:ipns:offline-datastore:error') + +// Offline datastore aims to mimic the same encoding as routing when storing records +// to the local datastore +class OfflineDatastore { + constructor (repo) { + this._repo = repo + } + + /** + * Put a value to the local datastore indexed by the received key properly encoded. + * @param {Buffer} key identifier of the value. + * @param {Buffer} value value to be stored. + * @param {function(Error)} callback + * @returns {void} + */ + put (key, value, callback) { + if (!Buffer.isBuffer(key)) { + const errMsg = `Offline datastore key must be a buffer` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_KEY')) + } + + if (!Buffer.isBuffer(value)) { + const errMsg = `Offline datastore value must be a buffer` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_VALUE')) + } + + let routingKey + + try { + routingKey = this._routingKey(key) + } catch (err) { + const errMsg = `Not possible to generate the routing key` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_GENERATING_ROUTING_KEY')) + } + + this._repo.datastore.put(routingKey, value, callback) + } + + /** + * Get a value from the local datastore indexed by the received key properly encoded. + * @param {Buffer} key identifier of the value to be obtained. + * @param {function(Error, Buffer)} callback + * @returns {void} + */ + get (key, callback) { + if (!Buffer.isBuffer(key)) { + const errMsg = `Offline datastore key must be a buffer` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_KEY')) + } + + let routingKey + + try { + routingKey = this._routingKey(key) + } catch (err) { + const errMsg = `Not possible to generate the routing key` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_GENERATING_ROUTING_KEY')) + } + + this._repo.datastore.get(routingKey, callback) + } + + // encode key properly - base32(/ipns/{cid}) + _routingKey (key) { + return new Key('/' + encodeBase32(key), false) + } +} + +exports = module.exports = OfflineDatastore diff --git a/src/core/ipns/routing/utils.js b/src/core/ipns/routing/utils.js new file mode 100644 index 0000000000..baa9ac16aa --- /dev/null +++ b/src/core/ipns/routing/utils.js @@ -0,0 +1,7 @@ +'use strict' + +const multibase = require('multibase') + +module.exports.encodeBase32 = (buf) => { + return multibase.encode('base32', buf).slice(1) // slice off multibase codec +} diff --git a/test/cli/name.js b/test/cli/name.js index 12e98d46cd..3acfb7b089 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -35,7 +35,7 @@ describe('name', () => { config: { Bootstrap: [] }, - args: pass.split(' '), + args: ['--pass', passPhrase, '--local'], initOptions: { bits: 512 } }, (err, _ipfsd) => { expect(err).to.not.exist() diff --git a/test/core/name.js b/test/core/name.js index 3bab36ece0..790aee384d 100644 --- a/test/core/name.js +++ b/test/core/name.js @@ -1,4 +1,4 @@ -/* eslint max-nested-callbacks: ["error", 6] */ +/* eslint max-nested-callbacks: ["error", 7] */ /* eslint-env mocha */ 'use strict' @@ -7,12 +7,14 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +const sinon = require('sinon') const fs = require('fs') const isNode = require('detect-node') const IPFS = require('../../src') const ipnsPath = require('../../src/core/ipns/path') +const { Key } = require('interface-datastore') const DaemonFactory = require('ipfsd-ctl') const df = DaemonFactory.create({ type: 'proc' }) @@ -24,191 +26,402 @@ describe('name', function () { return } - let node - let nodeId - let ipfsd - - before(function (done) { - this.timeout(40 * 1000) - df.spawn({ - exec: IPFS, - args: [`--pass ${hat()}`], - config: { Bootstrap: [] } - }, (err, _ipfsd) => { - expect(err).to.not.exist() - ipfsd = _ipfsd - node = _ipfsd.api - - node.id().then((res) => { - expect(res.id).to.exist() - - nodeId = res.id - done() - }) - }) - }) + describe('working locally', function () { + let node + let nodeId + let ipfsd + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`], + config: { Bootstrap: [] } + }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + node = _ipfsd.api - after((done) => ipfsd.stop(done)) + node.id().then((res) => { + expect(res.id).to.exist() - it('should publish correctly with the default options', function (done) { - node.name.publish(ipfsRef, { resolve: false }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - expect(res.name).to.equal(nodeId) - done() + nodeId = res.id + done() + }) + }) }) - }) - it('should publish and then resolve correctly with the default options', function (done) { - node.name.publish(ipfsRef, { resolve: false }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() + after((done) => ipfsd.stop(done)) - node.name.resolve(nodeId, (err, res) => { + it('should publish and then resolve correctly with the default options', function (done) { + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res.path).to.equal(ipfsRef) - done() + + node.name.resolve(nodeId, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) + done() + }) }) }) - }) - it('should publish correctly with the lifetime option and resolve', function (done) { - node.name.publish(ipfsRef, { resolve: false, lifetime: '2h' }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - - node.name.resolve(nodeId, (err, res) => { + it('should publish correctly with the lifetime option and resolve', function (done) { + node.name.publish(ipfsRef, { resolve: false, lifetime: '2h' }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res.path).to.equal(ipfsRef) - done() - }) - }) - }) - it('should not get the entry correctly if its validity time expired', function (done) { - node.name.publish(ipfsRef, { resolve: false, lifetime: '1ms' }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - - setTimeout(function () { - node.name.resolve(nodeId, (err) => { - expect(err).to.exist() + node.name.resolve(nodeId, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) done() }) - }, 2) + }) }) - }) - it('should recursively resolve to an IPFS hash', function (done) { - this.timeout(80 * 1000) - const keyName = hat() + it('should not get the entry correctly if its validity time expired', function (done) { + node.name.publish(ipfsRef, { resolve: false, lifetime: '1ms' }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() - node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { - expect(err).to.not.exist() + setTimeout(function () { + node.name.resolve(nodeId, (err) => { + expect(err).to.exist() + done() + }) + }, 2) + }) + }) + + it('should recursively resolve to an IPFS hash', function (done) { + this.timeout(80 * 1000) + const keyName = hat() - node.name.publish(ipfsRef, { resolve: false }, (err) => { + node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { expect(err).to.not.exist() - node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { + node.name.publish(ipfsRef, { resolve: false }, (err) => { expect(err).to.not.exist() - node.name.resolve(key.id, { recursive: true }, (err, res) => { + node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { expect(err).to.not.exist() - expect(res).to.exist() - expect(res.path).to.equal(ipfsRef) - done() + + node.name.resolve(key.id, { recursive: true }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) + done() + }) }) }) }) }) - }) - - it('should not recursively resolve to an IPFS hash if the option recursive is not provided', function (done) { - this.timeout(80 * 1000) - const keyName = hat() - node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { - expect(err).to.not.exist() + it('should not recursively resolve to an IPFS hash if the option recursive is not provided', function (done) { + this.timeout(80 * 1000) + const keyName = hat() - node.name.publish(ipfsRef, { resolve: false }, (err) => { + node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { expect(err).to.not.exist() - node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { + node.name.publish(ipfsRef, { resolve: false }, (err) => { expect(err).to.not.exist() - node.name.resolve(key.id, (err, res) => { + node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { expect(err).to.not.exist() - expect(res).to.exist() - expect(res.path).to.equal(`/ipns/${nodeId}`) - done() + + node.name.resolve(key.id, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(`/ipns/${nodeId}`) + done() + }) }) }) }) }) }) -}) -describe('ipns.path', function () { - const path = 'test/fixtures/planets/solar-system.md' - const fixture = { - path, - content: fs.readFileSync(path) - } + describe('republisher', function () { + if (!isNode) { + return + } + + let node + let ipfsd + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`], + config: { Bootstrap: [] } + }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + node = _ipfsd.api - let node - let ipfsd - let nodeId + done() + }) + }) - if (!isNode) { - return - } + afterEach(() => { + sinon.restore() + }) - before(function (done) { - this.timeout(40 * 1000) - df.spawn({ - exec: IPFS, - args: [`--pass ${hat()}`], - config: { Bootstrap: [] } - }, (err, _ipfsd) => { - expect(err).to.not.exist() - node = _ipfsd.api - ipfsd = _ipfsd - - node.id().then((res) => { - expect(res.id).to.exist() - - nodeId = res.id + after((done) => ipfsd.stop(done)) + + it('should republish entries after 60 seconds', function (done) { + this.timeout(100 * 1000) + sinon.spy(node._ipns.republisher, '_republishEntries') + + setTimeout(function () { + expect(node._ipns.republisher._republishEntries.calledOnce).to.equal(true) done() - }) + }, 60 * 1000) }) - }) - after((done) => ipfsd.stop(done)) + it('should error if run republish again', function (done) { + this.timeout(100 * 1000) + sinon.spy(node._ipns.republisher, '_republishEntries') + + try { + node._ipns.republisher.start() + } catch (err) { + expect(err).to.exist() + expect(err.code).to.equal('ERR_REPUBLISH_ALREADY_RUNNING') // already runs when starting + done() + } + }) + }) - it('should resolve an ipfs path correctly', function (done) { - node.files.add(fixture, (err, res) => { - expect(err).to.not.exist() - ipnsPath.resolvePath(node, `/ipfs/${res[0].hash}`, (err, value) => { + describe('errors', function () { + if (!isNode) { + return + } + + let node + let nodeId + let ipfsd + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`], + config: { Bootstrap: [] } + }, (err, _ipfsd) => { expect(err).to.not.exist() - expect(value).to.exist() + ipfsd = _ipfsd + node = _ipfsd.api + + node.id().then((res) => { + expect(res.id).to.exist() + + nodeId = res.id + done() + }) + }) + }) + + after((done) => ipfsd.stop(done)) + + it('should error to publish if does not receive private key', function (done) { + node._ipns.publisher.publish(null, ipfsRef, (err) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_UNDEFINED_PARAMETER') + done() + }) + }) + + it('should error to publish if an invalid private key is received', function (done) { + node._ipns.publisher.publish({ bytes: 'not that valid' }, ipfsRef, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('should error to publish if _updateOrCreateRecord fails', function (done) { + const stub = sinon.stub(node._ipns.publisher, '_updateOrCreateRecord').callsArgWith(4, 'error') + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.exist() + + stub.restore() + done() + }) + }) + + it('should error to publish if _putRecordToRouting receives an invalid peer id', function (done) { + node._ipns.publisher._putRecordToRouting(undefined, undefined, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('should error to publish if receives an invalid datastore key', function (done) { + const stub = sinon.stub(Key, 'isKey').returns(false) + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_INVALID_DATASTORE_KEY') + + stub.restore() + done() + }) + }) + + it('should error to publish if we receive a unexpected error getting from datastore', function (done) { + const stub = sinon.stub(node._ipns.publisher._repo.datastore, 'get').callsArgWith(1, 'error-unexpected') + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_UNEXPECTED_DATASTORE_RESPONSE') + + stub.restore() done() }) }) + + it('should error to publish if we receive a unexpected error putting to datastore', function (done) { + const stub = sinon.stub(node._ipns.publisher._repo.datastore, 'put').callsArgWith(2, 'error-unexpected') + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_STORING_IN_DATASTORE') + + stub.restore() + done() + }) + }) + + it('should error to resolve if the received name is not a string', function (done) { + node._ipns.resolver.resolve(false, (err) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_INVALID_PARAMETER') + done() + }) + }) + + it('should error to resolve if receives an invalid ipns path', function (done) { + node._ipns.resolver.resolve('ipns/', (err) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_INVALID_NAME_SYNTAX') + done() + }) + }) + + it('should publish and then fail to resolve if receive error getting from datastore', function (done) { + const stub = sinon.stub(node._ipns.resolver._routing, 'get').callsArgWith(1, 'error-unexpected') + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, { nocache: true }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_UNEXPECTED_ERROR_GETTING_RECORD') + stub.restore() + done() + }) + }) + }) + + it('should publish and then fail to resolve if does not find the record', function (done) { + const stub = sinon.stub(node._ipns.resolver._routing, 'get').callsArgWith(1, { code: 'ERR_NOT_FOUND' }) + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, { nocache: true }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_NO_RECORD_FOUND') + stub.restore() + done() + }) + }) + }) + + it('should publish and then fail to resolve if does not receive a buffer', function (done) { + const stub = sinon.stub(node._ipns.resolver._routing, 'get').callsArgWith(1, undefined, 'data') + + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, { nocache: true }, (err, res) => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_INVALID_RECORD_RECEIVED') + stub.restore() + done() + }) + }) + }) }) - it('should resolve an ipns path correctly', function (done) { - node.files.add(fixture, (err, res) => { - expect(err).to.not.exist() - node.name.publish(`/ipfs/${res[0].hash}`, (err, res) => { + describe('ipns.path', function () { + const path = 'test/fixtures/planets/solar-system.md' + const fixture = { + path, + content: fs.readFileSync(path) + } + + let node + let ipfsd + let nodeId + + if (!isNode) { + return + } + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`], + config: { Bootstrap: [] } + }, (err, _ipfsd) => { + expect(err).to.not.exist() + node = _ipfsd.api + ipfsd = _ipfsd + + node.id().then((res) => { + expect(res.id).to.exist() + + nodeId = res.id + done() + }) + }) + }) + + after((done) => ipfsd.stop(done)) + + it('should resolve an ipfs path correctly', function (done) { + node.files.add(fixture, (err, res) => { expect(err).to.not.exist() - ipnsPath.resolvePath(node, `/ipns/${nodeId}`, (err, value) => { + ipnsPath.resolvePath(node, `/ipfs/${res[0].hash}`, (err, value) => { expect(err).to.not.exist() expect(value).to.exist() done() }) }) }) + + it('should resolve an ipns path correctly', function (done) { + node.files.add(fixture, (err, res) => { + expect(err).to.not.exist() + node.name.publish(`/ipfs/${res[0].hash}`, (err, res) => { + expect(err).to.not.exist() + ipnsPath.resolvePath(node, `/ipns/${nodeId}`, (err, value) => { + expect(err).to.not.exist() + expect(value).to.exist() + done() + }) + }) + }) + }) }) }) diff --git a/test/core/node.js b/test/core/node.js index be56f4b53c..3ddfaef962 100644 --- a/test/core/node.js +++ b/test/core/node.js @@ -1,6 +1,7 @@ 'use strict' require('./circuit-relay') +require('./name') require('./key-exchange') require('./pin') require('./pin-set')