From 5044a300afc04df83041364cb68d6f2ce9636c69 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Wed, 26 Jun 2019 23:37:38 +0200 Subject: [PATCH] feat: add support for ipns name resolve /ipns/ (#2002) fixes: #1918 `ipns name resolve` dns tests moved to interface-core resolve call now returns a string as per documention. --- .travis.yml | 6 +- package.json | 9 +- src/cli/commands/name/publish.js | 22 +- src/cli/commands/name/resolve.js | 8 +- src/core/components/name.js | 66 ++++-- src/core/index.js | 12 +- src/core/ipns/index.js | 34 +-- src/core/ipns/publisher.js | 5 +- src/http/api/resources/name.js | 4 +- src/utils/tlru.js | 87 +++++++ test/cli/name-pubsub.js | 50 ----- test/cli/name.js | 336 ++++------------------------ test/core/files-regular-utils.js | 33 +-- test/core/interface.spec.js | 34 +-- test/core/kad-dht.node.js | 2 +- test/core/name-pubsub.js | 33 ++- test/core/{name.js => name.spec.js} | 126 +---------- test/core/node.js | 1 - test/http-api/interface.js | 24 ++ test/utils/dns-fetch-stub.js | 16 -- 20 files changed, 311 insertions(+), 597 deletions(-) create mode 100644 src/utils/tlru.js rename test/core/{name.js => name.spec.js} (80%) delete mode 100644 test/utils/dns-fetch-stub.js diff --git a/.travis.yml b/.travis.yml index 19ece97bfc..995a49a2f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ os: - osx - windows -script: npx nyc -s npm run test:node --timeout=10000 -- --bail +script: npx nyc -s npx aegir test -t node --timeout 10000 --bail after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov jobs: @@ -47,12 +47,12 @@ jobs: - stage: test name: electron-main script: - - xvfb-run npx aegir test -t electron-main -- --bail + - xvfb-run npx aegir test -t electron-main -- --bail --timeout 10000 - stage: test name: electron-renderer script: - - xvfb-run npx aegir test -t electron-renderer -- --bail + - xvfb-run npx aegir test -t electron-renderer -- --bail --timeout 10000 notifications: email: false diff --git a/package.json b/package.json index 68067f570f..ad8413336d 100644 --- a/package.json +++ b/package.json @@ -87,12 +87,13 @@ "get-folder-size": "^2.0.0", "glob": "^7.1.3", "hapi-pino": "^6.0.0", + "hashlru": "^2.3.0", "human-to-milliseconds": "^1.0.0", "interface-datastore": "~0.6.0", "ipfs-bitswap": "~0.24.1", "ipfs-block": "~0.8.1", "ipfs-block-service": "~0.15.1", - "ipfs-http-client": "^32.0.0", + "ipfs-http-client": "^32.0.1", "ipfs-http-response": "~0.3.0", "ipfs-mfs": "~0.11.4", "ipfs-multipart": "~0.1.0", @@ -110,6 +111,7 @@ "ipld-raw": "^4.0.0", "ipld-zcash": "~0.3.0", "ipns": "~0.5.2", + "is-domain-name": "^1.0.1", "is-ipfs": "~0.6.1", "is-pull-stream": "~0.0.0", "is-stream": "^2.0.0", @@ -176,6 +178,7 @@ "aegir": "^19.0.3", "base64url": "^3.0.1", "chai": "^4.2.0", + "clear-module": "^3.2.0", "delay": "^4.1.0", "detect-node": "^2.0.4", "dir-compare": "^1.4.0", @@ -183,8 +186,8 @@ "execa": "^1.0.0", "form-data": "^2.3.3", "hat": "0.0.3", - "interface-ipfs-core": "~0.104.0", - "ipfsd-ctl": "~0.42.0", + "interface-ipfs-core": "~0.105.0", + "ipfsd-ctl": "~0.43.0", "libp2p-websocket-star": "~0.10.2", "ncp": "^2.0.0", "qs": "^6.5.2", diff --git a/src/cli/commands/name/publish.js b/src/cli/commands/name/publish.js index 8452f23248..b84227a5d6 100644 --- a/src/cli/commands/name/publish.js +++ b/src/cli/commands/name/publish.js @@ -1,6 +1,6 @@ 'use strict' -const print = require('../../utils').print +const { print } = require('../../utils') module.exports = { command: 'publish ', @@ -11,21 +11,25 @@ module.exports = { resolve: { alias: 'r', describe: 'Resolve given path before publishing. Default: true.', - default: true + default: true, + type: 'boolean' }, lifetime: { alias: 't', describe: 'Time duration that the record will be valid for. Default: 24h.', - default: '24h' + default: '24h', + type: 'string' }, key: { alias: 'k', describe: 'Name of the key to be used, as listed by "ipfs key list -l". Default: self.', - default: 'self' + default: 'self', + type: 'string' }, ttl: { describe: 'Time duration this record should be cached for (caution: experimental).', - default: '' + default: '', + type: 'string' } }, @@ -33,14 +37,8 @@ module.exports = { argv.resolve((async () => { // yargs-promise adds resolve/reject properties to argv // resolve should use the alias as resolve will always be overwritten to a function - let resolve = true - - if (argv.r === false || argv.r === 'false') { - resolve = false - } - const opts = { - resolve, + resolve: argv.r, lifetime: argv.lifetime, key: argv.key, ttl: argv.ttl diff --git a/src/cli/commands/name/resolve.js b/src/cli/commands/name/resolve.js index e8871a1d17..ca1dd219a3 100644 --- a/src/cli/commands/name/resolve.js +++ b/src/cli/commands/name/resolve.js @@ -18,7 +18,7 @@ module.exports = { type: 'boolean', alias: 'r', describe: 'Resolve until the result is not an IPNS name. Default: false.', - default: false + default: true } }, @@ -32,11 +32,7 @@ module.exports = { const ipfs = await argv.getIpfs() const result = await ipfs.name.resolve(argv.name, opts) - if (result && result.path) { - print(result.path) - } else { - print(result) - } + print(result) })()) } } diff --git a/src/core/components/name.js b/src/core/components/name.js index 239d8ecc32..aee8dfba29 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -7,6 +7,9 @@ const parallel = require('async/parallel') const human = require('human-to-milliseconds') const crypto = require('libp2p-crypto') const errcode = require('err-code') +const mergeOptions = require('merge-options') +const mh = require('multihashes') +const isDomain = require('is-domain-name') const log = debug('ipfs:name') log.error = debug('ipfs:name:error') @@ -35,6 +38,28 @@ const keyLookup = (ipfsNode, kname, callback) => { }) } +const appendRemainder = (cb, remainder) => { + return (err, result) => { + if (err) { + return cb(err) + } + if (remainder.length) { + return cb(null, result + '/' + remainder.join('/')) + } + return cb(null, result) + } +} + +/** + * @typedef { import("../index") } IPFS + */ + +/** + * IPNS - Inter-Planetary Naming System + * + * @param {IPFS} self + * @returns {Object} + */ module.exports = function name (self) { return { /** @@ -125,22 +150,15 @@ module.exports = function name (self) { options = {} } - options = options || {} - const nocache = options.nocache && options.nocache.toString() === 'true' - const recursive = options.recursive && options.recursive.toString() === 'true' + options = mergeOptions({ + nocache: false, + recursive: true + }, options) const offline = self._options.offline - if (!self.isOnline() && !offline) { - const errMsg = utils.OFFLINE_ERROR - - log.error(errMsg) - return callback(errcode(errMsg, 'OFFLINE_ERROR')) - } - // TODO: params related logic should be in the core implementation - - if (offline && nocache) { + if (offline && options.nocache) { const error = 'cannot specify both offline and nocache' log.error(error) @@ -156,12 +174,28 @@ module.exports = function name (self) { name = `/ipns/${name}` } - const resolveOptions = { - nocache, - recursive + const [ namespace, hash, ...remainder ] = name.slice(1).split('/') + try { + mh.fromB58String(hash) + } catch (err) { + // lets check if we have a domain ex. /ipns/ipfs.io and resolve with dns + if (isDomain(hash)) { + return self.dns(hash, options, appendRemainder(callback, remainder)) + } + + log.error(err) + return callback(errcode(new Error('Invalid IPNS name.'), 'ERR_IPNS_INVALID_NAME')) } - self._ipns.resolve(name, resolveOptions, callback) + // multihash is valid lets resolve with IPNS + // IPNS resolve needs a online daemon + if (!self.isOnline() && !offline) { + const errMsg = utils.OFFLINE_ERROR + + log.error(errMsg) + return callback(errcode(errMsg, 'OFFLINE_ERROR')) + } + self._ipns.resolve(`/${namespace}/${hash}`, options, appendRemainder(callback, remainder)) }), pubsub: namePubsub(self) } diff --git a/src/core/index.js b/src/core/index.js index d457cb6149..3245a6cb9a 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,7 +26,16 @@ const defaultRepo = require('./runtime/repo-nodejs') const preload = require('./preload') const mfsPreload = require('./mfs-preload') const ipldOptions = require('./runtime/ipld-nodejs') - +/** + * @typedef { import("./ipns/index") } IPNS + */ + +/** + * + * + * @class IPFS + * @extends {EventEmitter} + */ class IPFS extends EventEmitter { constructor (options) { super() @@ -76,6 +85,7 @@ class IPFS extends EventEmitter { this._ipld = new Ipld(ipldOptions(this._blockService, this._options.ipld, this.log)) this._preload = preload(this) this._mfsPreload = mfsPreload(this) + /** @type {IPNS} */ this._ipns = undefined // eslint-disable-next-line no-console this._print = this._options.silent ? this.log : console.log diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index a064ece4d5..d7405ca3e7 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -2,7 +2,6 @@ const { createFromPrivKey } = require('peer-id') const series = require('async/series') -const Receptacle = require('receptacle') const errcode = require('err-code') const debug = require('debug') @@ -13,7 +12,8 @@ const IpnsPublisher = require('./publisher') const IpnsRepublisher = require('./republisher') const IpnsResolver = require('./resolver') const path = require('./path') - +const { normalizePath } = require('../utils') +const TLRU = require('../../utils/tlru') const defaultRecordTtl = 60 * 1000 class IPNS { @@ -21,12 +21,19 @@ class IPNS { this.publisher = new IpnsPublisher(routing, datastore) this.republisher = new IpnsRepublisher(this.publisher, datastore, peerInfo, keychain, options) this.resolver = new IpnsResolver(routing) - this.cache = new Receptacle({ max: 1000 }) // Create an LRU cache with max 1000 items + this.cache = new TLRU(1000) this.routing = routing } // Publish - publish (privKey, value, lifetime, callback) { + publish (privKey, value, lifetime = IpnsPublisher.defaultRecordLifetime, callback) { + try { + value = normalizePath(value) + } catch (err) { + log.error(err) + return callback(err) + } + series([ (cb) => createFromPrivKey(privKey.bytes, cb), (cb) => this.publisher.publishWithEOL(privKey, value, lifetime, cb) @@ -38,12 +45,12 @@ class IPNS { log(`IPNS value ${value} was published correctly`) - // Add to cache + // // Add to cache const id = results[0].toB58String() const ttEol = parseFloat(lifetime) const ttl = (ttEol < defaultRecordTtl) ? ttEol : defaultRecordTtl - this.cache.set(id, value, { ttl: ttl }) + this.cache.set(id, value, ttl) log(`IPNS value ${value} was cached correctly`) @@ -77,9 +84,7 @@ class IPNS { const result = this.cache.get(id) if (result) { - return callback(null, { - path: result - }) + return callback(null, result) } } @@ -91,18 +96,17 @@ class IPNS { log(`IPNS record from ${name} was resolved correctly`) - callback(null, { - path: result - }) + callback(null, result) }) } // Initialize keyspace // sets the ipns record for the given key to point to an empty directory initializeKeyspace (privKey, value, callback) { - this.publisher.publish(privKey, value, callback) + this.publish(privKey, value, IpnsPublisher.defaultRecordLifetime, callback) } } -exports = module.exports = IPNS -exports.path = path +IPNS.path = path + +module.exports = IPNS diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 4a3fb1a4de..1137d006cb 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -11,7 +11,7 @@ log.error = debug('ipfs:ipns:publisher:error') const ipns = require('ipns') -const defaultRecordTtl = 60 * 60 * 1000 +const defaultRecordLifetime = 60 * 60 * 1000 // IpnsPublisher is capable of publishing and resolving names to the IPFS routing system. class IpnsPublisher { @@ -46,7 +46,7 @@ class IpnsPublisher { // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system publish (privKey, value, callback) { - this.publishWithEOL(privKey, value, defaultRecordTtl, callback) + this.publishWithEOL(privKey, value, defaultRecordLifetime, callback) } _putRecordToRouting (record, peerId, callback) { @@ -269,4 +269,5 @@ class IpnsPublisher { } } +IpnsPublisher.defaultRecordLifetime = defaultRecordLifetime exports = module.exports = IpnsPublisher diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index 7c8206444c..02b1eb7326 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -7,7 +7,7 @@ exports.resolve = { query: Joi.object().keys({ arg: Joi.string(), nocache: Joi.boolean().default(false), - recursive: Joi.boolean().default(false) + recursive: Joi.boolean().default(true) }).unknown() }, async handler (request, h) { @@ -17,7 +17,7 @@ exports.resolve = { const res = await ipfs.name.resolve(arg, request.query) return h.response({ - Path: res.path + Path: res }) } } diff --git a/src/utils/tlru.js b/src/utils/tlru.js new file mode 100644 index 0000000000..ba3b26e8c6 --- /dev/null +++ b/src/utils/tlru.js @@ -0,0 +1,87 @@ +'use strict' +const hashlru = require('hashlru') + +/** + * Time Aware Least Recent Used Cache + * @see https://arxiv.org/pdf/1801.00390 + * @todo move this to ipfs-utils or it's own package + * + * @class TLRU + */ +class TLRU { + /** + * Creates an instance of TLRU. + * + * @param {number} maxSize + * @memberof TLRU + */ + constructor (maxSize) { + this.lru = hashlru(maxSize) + } + + /** + * Get the value from the a key + * + * @param {string} key + * @returns {any} + * @memberof TLRU + */ + get (key) { + const value = this.lru.get(key) + if (value) { + if ((value.expire) && (value.expire < Date.now())) { + this.lru.remove(key) + return undefined + } + } + return value.value + } + + /** + * Set a key value pair + * + * @param {string} key + * @param {any} value + * @param {number} ttl - in miliseconds + * @memberof TLRU + */ + set (key, value, ttl) { + this.lru.set(key, { value, expire: Date.now() + ttl }) + } + + /** + * Find if the cache has the key + * + * @param {string} key + * @returns {boolean} + * @memberof TLRU + */ + has (key) { + const value = this.get(key) + if (value) { + return true + } + return false + } + + /** + * Remove key + * + * @param {string} key + * @memberof TLRU + */ + remove (key) { + this.lru.remove(key) + } + + /** + * Clears the cache + * + * @memberof TLRU + */ + clear () { + this.lru.clear() + } +} + +module.exports = TLRU diff --git a/test/cli/name-pubsub.js b/test/cli/name-pubsub.js index fef64295de..cfeb5cf140 100644 --- a/test/cli/name-pubsub.js +++ b/test/cli/name-pubsub.js @@ -14,9 +14,6 @@ const ipfsExec = require('../utils/ipfs-exec') const DaemonFactory = require('ipfsd-ctl') const df = DaemonFactory.create({ type: 'js' }) -const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) -const emptyDirCid = 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' - const spawnDaemon = (callback) => { df.spawn({ exec: path.resolve(`${__dirname}/../../src/cli/bin.js`), @@ -165,53 +162,6 @@ describe('name-pubsub', () => { }) }) }) - - describe('pubsub records', () => { - let cidAdded - - before(function (done) { - this.timeout(50 * 1000) - ipfsA(`add ${path.resolve(`${__dirname}/../../src/init-files/init-docs/readme`)}`) - .then((out) => { - cidAdded = out.split(' ')[1] - done() - }) - }) - - it('should publish the received record to the subscriber', function () { - this.timeout(80 * 1000) - - return ipfsB(`name resolve ${nodeBId.id}`) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([emptyDirCid])) // Empty dir received (subscribed) - - return ipfsA(`name resolve ${nodeBId.id}`) - }) - .catch((err) => { - expect(err).to.exist() // Not available (subscribed now) - - return ipfsB(`name publish ${cidAdded}`) - }) - .then((res) => { - // published to IpfsB and published through pubsub to ipfsa - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, nodeBId.id])) - - return ipfsB(`name resolve ${nodeBId.id}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) - - return ipfsA(`name resolve ${nodeBId.id}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) // value propagated to node B - }) - }) - }) }) describe('disabled', () => { diff --git a/test/cli/name.js b/test/cli/name.js index 1c0638503f..984d777da8 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -1,305 +1,59 @@ -/* eslint max-nested-callbacks: ["error", 6] */ /* eslint-env mocha */ 'use strict' -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const path = require('path') -const hat = require('hat') -const ipfsExec = require('../utils/ipfs-exec') - -const DaemonFactory = require('ipfsd-ctl') -const df = DaemonFactory.create({ type: 'js' }) - -const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) +const sinon = require('sinon') +const YargsPromise = require('yargs-promise') +const clearModule = require('clear-module') describe('name', () => { - describe('working locally', () => { - const passPhrase = hat() - const pass = '--pass ' + passPhrase - const name = 'test-key-' + hat() - - let ipfs - let ipfsd - - let cidAdded - let nodeId - let keyId - - before(function (done) { - this.timeout(80 * 1000) - - df.spawn({ - exec: path.resolve(`${__dirname}/../../src/cli/bin.js`), - config: { - Bootstrap: [] - }, - args: ['--pass', passPhrase, '--offline'], - initOptions: { bits: 512 } - }, (err, _ipfsd) => { - expect(err).to.not.exist() - - ipfsd = _ipfsd - ipfs = ipfsExec(_ipfsd.repoPath) - - ipfs(`${pass} key gen ${name} --type rsa --size 2048`) - .then((out) => { - expect(out).to.include(name) - keyId = out.split(' ')[1] - - return ipfs('id') - }) - .then((res) => { - const id = JSON.parse(res) - expect(id).to.have.property('id') - nodeId = id.id - - return ipfs(`add ${path.resolve(`${__dirname}/../../src/init-files/init-docs/readme`)}`) - }) - .then((out) => { - cidAdded = out.split(' ')[1] - done() - }) - }) - }) - - after(function (done) { - if (ipfsd) { - ipfsd.stop(() => done()) - } else { - done() - } - }) - - it('should publish correctly when the file was already added', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded}`).then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, nodeId])) - }) - }) - - it('should publish and resolve an entry with the default options', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded}`) - .then((res) => { - expect(res).to.exist() - - return ipfs('name resolve') - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) - }) - }) - - it('should publish correctly when the file was not added but resolve is disabled', function () { - this.timeout(70 * 1000) - - const notAddedCid = 'QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' - - return ipfs(`name publish ${notAddedCid} --resolve false`).then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([notAddedCid, nodeId])) - }) - }) - - it('should not get the entry correctly if its validity time expired', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded} --lifetime 10ns`) - .then((res) => { - expect(res).to.exist() - - setTimeout(function () { - return ipfs('name resolve') - .then((res) => { - expect(res).to.not.exist() - }) - .catch((err) => { - expect(err).to.exist() - }) - }, 1) - }) - }) - - it('should publish correctly when a new key is used', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded} --key ${name}`).then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, keyId])) - }) - }) - - it('should return the immediate pointing record, unless using the recursive parameter', function () { - this.timeout(90 * 1000) - - return ipfs(`name publish ${cidAdded}`) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, nodeId])) - - return ipfs(`name publish /ipns/${nodeId} --key ${name}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([nodeId, keyId])) - - return ipfs(`name resolve ${keyId}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([nodeId])) - }) - }) - - it('should go recursively until finding an ipfs hash', function () { - this.timeout(90 * 1000) - - return ipfs(`name publish ${cidAdded}`) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, nodeId])) - - return ipfs(`name publish /ipns/${nodeId} --key ${name}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([nodeId, keyId])) - - return ipfs(`name resolve ${keyId} --recursive`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) - }) - }) + let cli + let cliUtils + beforeEach(() => { + cliUtils = require('../../src/cli/utils') + cli = new YargsPromise(require('../../src/cli/parser')) + }) + afterEach(() => { + sinon.restore() + // TODO: the lines below shouldn't be necessary, cli needs refactor to simplify testability + // Force the next require to not use require cache + clearModule('../../src/cli/utils') + clearModule('../../src/cli/parser') }) - describe('using dht', () => { - const passPhrase = hat() - const pass = '--pass ' + passPhrase - const name = 'test-key-' + hat() - - let ipfs - let ipfsd - - let cidAdded - let nodeId - let keyId - - before(function (done) { - this.timeout(80 * 1000) - - df.spawn({ - exec: path.resolve(`${__dirname}/../../src/cli/bin.js`), - config: { - Bootstrap: [], - Discovery: { - MDNS: { - Enabled: false - }, - webRTCStar: { - Enabled: false - } - } - }, - args: ['--pass', passPhrase], - initOptions: { bits: 512 } - }, (err, _ipfsd) => { - expect(err).to.not.exist() - - ipfsd = _ipfsd - ipfs = ipfsExec(_ipfsd.repoPath) - - ipfs(`${pass} key gen ${name} --type rsa --size 2048`) - .then((out) => { - expect(out).to.include(name) - keyId = out.split(' ')[1] - - return ipfs('id') - }) - .then((res) => { - const id = JSON.parse(res) - expect(id).to.have.property('id') - nodeId = id.id - - return ipfs(`add ${path.resolve(`${__dirname}/../../src/init-files/init-docs/readme`)}`) - }) - .then((out) => { - cidAdded = out.split(' ')[1] - done() - }) - }) - }) - - after(function (done) { - if (ipfsd) { - ipfsd.stop(() => done()) - } else { - done() - } - }) - - it('should publish and resolve an entry with the default options', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded}`) - .then((res) => { - expect(res).to.exist() - - return ipfs('name resolve') - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) - }) - }) - - it('should not get the entry correctly if its validity time expired', function () { - this.timeout(70 * 1000) - - return ipfs(`name publish ${cidAdded} --lifetime 10ns`) - .then((res) => { - expect(res).to.exist() - - setTimeout(function () { - return ipfs('name resolve') - .then((res) => { - expect(res).to.not.exist() - }) - .catch((err) => { - expect(err).to.exist() - }) - }, 1) - }) - }) + it('resolve', async () => { + const resolveFake = sinon.fake() - it('should return the immediate pointing record, unless using the recursive parameter', function () { - this.timeout(90 * 1000) + sinon + .stub(cliUtils, 'getIPFS') + .callsArgWith(1, null, { name: { resolve: resolveFake } }) - return ipfs(`name publish ${cidAdded}`) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + // TODO: the lines below shouldn't be necessary, cli needs refactor to simplify testability + // Force the next require to not use require cache + clearModule('../../src/cli/commands/name/resolve.js') - return ipfs(`name publish /ipns/${nodeId} --key ${name}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([nodeId, keyId])) + await cli.parse(`name resolve test`) + sinon.assert.calledWith(resolveFake, 'test', { nocache: false, recursive: true }) + }) - return ipfs(`name resolve ${keyId}`) - }) - .then((res) => { - expect(res).to.exist() - expect(res).to.satisfy(checkAll([nodeId])) - }) + it('publish', async () => { + const publishFake = sinon.fake.returns({ name: 'name', value: 'value' }) + const printSpy = sinon.spy(cliUtils, 'print') + + sinon + .stub(cliUtils, 'getIPFS') + .callsArgWith(1, null, { name: { publish: publishFake } }) + + // TODO: the lines below shouldn't be necessary, cli needs refactor to simplify testability + // Force the next require to not use require cache + clearModule('../../src/cli/commands/name/publish.js') + + await cli.parse(`name publish test --silent`) + sinon.assert.calledWith(printSpy, 'Published to name: value') + sinon.assert.calledWith(publishFake, 'test', { + resolve: true, + lifetime: '24h', + key: 'self', + ttl: '' }) }) }) diff --git a/test/core/files-regular-utils.js b/test/core/files-regular-utils.js index 3b86020d3f..380ff3f3e8 100644 --- a/test/core/files-regular-utils.js +++ b/test/core/files-regular-utils.js @@ -12,47 +12,38 @@ describe('files-regular/utils', () => { describe('parseChunkerString', () => { it('handles an empty string', () => { const options = utils.parseChunkerString('') - expect(options).to.have.property('chunker').to.equal('fixed') + expect(options.chunker).to.equal('fixed') }) it('handles a null chunker string', () => { const options = utils.parseChunkerString(null) - expect(options).to.have.property('chunker').to.equal('fixed') + expect(options.chunker).to.equal('fixed') }) it('parses a fixed size string', () => { const options = utils.parseChunkerString('size-512') - expect(options).to.have.property('chunker').to.equal('fixed') - expect(options) - .to.have.property('chunkerOptions') - .to.have.property('maxChunkSize') - .to.equal(512) + expect(options.chunker).to.equal('fixed') + expect(options.chunkerOptions.maxChunkSize).to.equal(512) }) it('parses a rabin string without size', () => { const options = utils.parseChunkerString('rabin') - expect(options).to.have.property('chunker').to.equal('rabin') - expect(options) - .to.have.property('chunkerOptions') - .to.have.property('avgChunkSize') + expect(options.chunker).to.equal('rabin') + expect(options.chunkerOptions.avgChunkSize).to.equal(262144) }) it('parses a rabin string with only avg size', () => { const options = utils.parseChunkerString('rabin-512') - expect(options).to.have.property('chunker').to.equal('rabin') - expect(options) - .to.have.property('chunkerOptions') - .to.have.property('avgChunkSize') - .to.equal(512) + expect(options.chunker).to.equal('rabin') + expect(options.chunkerOptions.avgChunkSize).to.equal(512) }) it('parses a rabin string with min, avg, and max', () => { const options = utils.parseChunkerString('rabin-42-92-184') - expect(options).to.have.property('chunker').to.equal('rabin') - expect(options).to.have.property('chunkerOptions') - expect(options.chunkerOptions).to.have.property('minChunkSize').to.equal(42) - expect(options.chunkerOptions).to.have.property('avgChunkSize').to.equal(92) - expect(options.chunkerOptions).to.have.property('maxChunkSize').to.equal(184) + expect(options.chunker).to.equal('rabin') + expect(options.chunkerOptions.minChunkSize).to.equal(42) + expect(options.chunkerOptions.avgChunkSize).to.equal(92) + expect(options.chunkerOptions.maxChunkSize).to.equal(184) }) it('throws an error for unsupported chunker type', () => { diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index df572c19e2..a091dd0ea4 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -4,26 +4,10 @@ const tests = require('interface-ipfs-core') const CommonFactory = require('../utils/interface-common-factory') const isNode = require('detect-node') -const dnsFetchStub = require('../utils/dns-fetch-stub') describe('interface-ipfs-core tests', function () { this.timeout(20 * 1000) - // ipfs.dns in the browser calls out to https://ipfs.io/api/v0/dns. - // The following code stubs self.fetch to return a static CID for calls - // to https://ipfs.io/api/v0/dns?arg=ipfs.io. - if (!isNode) { - const fetch = self.fetch - - before(() => { - self.fetch = dnsFetchStub(fetch) - }) - - after(() => { - self.fetch = fetch - }) - } - const defaultCommonFactory = CommonFactory.create() tests.bitswap(defaultCommonFactory, { skip: !isNode }) @@ -107,29 +91,13 @@ describe('interface-ipfs-core tests', function () { { name: 'should resolve IPNS link recursively', reason: 'TODO: IPNS resolve not yet implemented https://github.com/ipfs/js-ipfs/issues/1918' - }, - { - name: 'should recursively resolve ipfs.io', - reason: 'TODO: ipfs.io dnslink=/ipns/website.ipfs.io & IPNS resolve not yet implemented https://github.com/ipfs/js-ipfs/issues/1918' } ] }) tests.name(CommonFactory.create({ spawnOptions: { - args: ['--pass ipfs-is-awesome-software', '--offline'], - initOptions: { bits: 512 }, - config: { - Bootstrap: [], - Discovery: { - MDNS: { - Enabled: false - }, - webRTCStar: { - Enabled: false - } - } - } + args: ['--pass ipfs-is-awesome-software', '--offline'] } })) diff --git a/test/core/kad-dht.node.js b/test/core/kad-dht.node.js index d07be33f20..d06392abbc 100644 --- a/test/core/kad-dht.node.js +++ b/test/core/kad-dht.node.js @@ -32,7 +32,7 @@ function createNode (callback) { }, callback) } -describe('kad-dht is routing content and peers correctly', () => { +describe.skip('kad-dht is routing content and peers correctly', () => { let nodeA let nodeB let nodeC diff --git a/test/core/name-pubsub.js b/test/core/name-pubsub.js index 575fb54fad..884d0a55b7 100644 --- a/test/core/name-pubsub.js +++ b/test/core/name-pubsub.js @@ -18,6 +18,7 @@ const isNode = require('detect-node') const ipns = require('ipns') const IPFS = require('../../src') const waitFor = require('../utils/wait-for') +const delay = require('interface-ipfs-core/src/utils/delay') const DaemonFactory = require('ipfsd-ctl') const df = DaemonFactory.create({ type: 'proc' }) @@ -34,6 +35,7 @@ describe('name-pubsub', function () { let nodeA let nodeB let idA + let idB const createNode = (callback) => { df.spawn({ @@ -73,6 +75,7 @@ describe('name-pubsub', function () { expect(err).to.not.exist() idA = ids[0] + idB = ids[1] nodeA.swarm.connect(ids[1].addresses[0], done) }) }) @@ -130,10 +133,36 @@ describe('name-pubsub', function () { expect(err).to.not.exist() expect(res).to.exist() - expect(res[5]).to.exist() - expect(res[5].path).to.equal(ipfsRef) + expect(res[5]).to.equal(ipfsRef) done() }) }) }) + + it('should self resolve, publish and then resolve correctly', async function () { + this.timeout(6000) + const emptyDirCid = '/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' + const [{ path }] = await nodeA.add(Buffer.from('pubsub records')) + + const resolvesEmpty = await nodeB.name.resolve(idB.id) + expect(resolvesEmpty).to.be.eq(emptyDirCid) + + try { + await nodeA.name.resolve(idB.id) + } catch (error) { + expect(error).to.exist() + } + + const publish = await nodeB.name.publish(path) + expect(publish).to.be.eql({ + name: idB.id, + value: `/ipfs/${path}` + }) + + const resolveB = await nodeB.name.resolve(idB.id) + expect(resolveB).to.be.eq(`/ipfs/${path}`) + await delay(5000) + const resolveA = await nodeA.name.resolve(idB.id) + expect(resolveA).to.be.eq(`/ipfs/${path}`) + }) }) diff --git a/test/core/name.js b/test/core/name.spec.js similarity index 80% rename from test/core/name.js rename to test/core/name.spec.js index 99b8257251..1191eba711 100644 --- a/test/core/name.js +++ b/test/core/name.spec.js @@ -9,11 +9,9 @@ const expect = chai.expect chai.use(dirtyChai) const sinon = require('sinon') -const fs = require('fs') const parallel = require('async/parallel') const series = require('async/series') -const isNode = require('detect-node') const IPFS = require('../../src') const ipnsPath = require('../../src/core/ipns/path') const ipnsRouting = require('../../src/core/ipns/routing/config') @@ -34,120 +32,13 @@ const publishAndResolve = (publisher, resolver, ipfsRef, publishOpts, nodeId, re expect(err).to.not.exist() expect(res[0]).to.exist() expect(res[1]).to.exist() - expect(res[1].path).to.equal(ipfsRef) + expect(res[1]).to.equal(ipfsRef) callback() }) } describe('name', function () { - if (!isNode) { - return - } - - describe('working locally', function () { - let node - let nodeId - let ipfsd - - before(function (done) { - this.timeout(50 * 1000) - df.spawn({ - exec: IPFS, - args: [`--pass ${hat()}`, '--offline'], - 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() - }) - }) - }) - - after((done) => ipfsd.stop(done)) - - it('should publish and then resolve correctly with the default options', function (done) { - this.timeout(50 * 1000) - - publishAndResolve(node, node, ipfsRef, { resolve: false }, nodeId, {}, done) - }) - - it('should publish correctly with the lifetime option and resolve', function (done) { - this.timeout(50 * 1000) - - const publishOpts = { - resolve: false, - lifetime: '2h' - } - - publishAndResolve(node, node, ipfsRef, publishOpts, nodeId, {}, done) - }) - - it('should not get the entry correctly if its validity time expired', function (done) { - this.timeout(50 * 1000) - - 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() - done() - }) - }, 2) - }) - }) - - it('should recursively resolve to an IPFS hash', function (done) { - this.timeout(90 * 1000) - const keyName = hat() - - node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { - expect(err).to.not.exist() - series([ - (cb) => node.name.publish(ipfsRef, { resolve: false }, cb), - (cb) => node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, cb), - (cb) => node.name.resolve(key.id, { recursive: true }, cb) - ], (err, res) => { - expect(err).to.not.exist() - expect(res[2]).to.exist() - expect(res[2].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(90 * 1000) - const keyName = hat() - - node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { - expect(err).to.not.exist() - series([ - (cb) => node.name.publish(ipfsRef, { resolve: false }, cb), - (cb) => node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, cb), - (cb) => node.name.resolve(key.id, cb) - ], (err, res) => { - expect(err).to.not.exist() - expect(res[2]).to.exist() - expect(res[2].path).to.equal(`/ipns/${nodeId}`) - done() - }) - }) - }) - }) - describe('republisher', function () { - if (!isNode) { - return - } - let node let ipfsd @@ -277,7 +168,7 @@ describe('name', function () { ], (err, res) => { expect(err).to.not.exist() expect(res[2]).to.exist() - expect(res[2].path).to.equal(ipfsRef) + expect(res[2]).to.equal(ipfsRef) done() }) }) @@ -285,10 +176,6 @@ describe('name', function () { }) describe('errors', function () { - if (!isNode) { - return - } - let node let nodeId let ipfsd @@ -460,20 +347,15 @@ describe('name', function () { }) describe('ipns.path', function () { - const path = 'test/fixtures/planets/solar-system.md' const fixture = { - path, - content: fs.readFileSync(path) + path: 'test/fixtures/planets/solar-system.md', + content: Buffer.from('ipns.path') } let node let ipfsd let nodeId - if (!isNode) { - return - } - before(function (done) { this.timeout(40 * 1000) df.spawn({ diff --git a/test/core/node.js b/test/core/node.js index b9297cce42..35f185f1d3 100644 --- a/test/core/node.js +++ b/test/core/node.js @@ -2,7 +2,6 @@ require('./circuit-relay') require('./files-regular-utils') -require('./name') require('./name-pubsub') require('./key-exchange') require('./pin') diff --git a/test/http-api/interface.js b/test/http-api/interface.js index 43c6ff6a12..d1607dfb71 100644 --- a/test/http-api/interface.js +++ b/test/http-api/interface.js @@ -96,6 +96,30 @@ describe('interface-ipfs-core over ipfs-http-client tests', () => { ] }) + tests.name(CommonFactory.create({ + spawnOptions: { + args: ['--pass ipfs-is-awesome-software', '--offline'] + } + })) + + tests.namePubsub(CommonFactory.create({ + spawnOptions: { + args: ['--enable-namesys-pubsub'], + initOptions: { bits: 1024 }, + config: { + Bootstrap: [], + Discovery: { + MDNS: { + Enabled: false + }, + webRTCStar: { + Enabled: false + } + } + } + } + })) + tests.object(defaultCommonFactory) tests.pin(defaultCommonFactory) diff --git a/test/utils/dns-fetch-stub.js b/test/utils/dns-fetch-stub.js deleted file mode 100644 index a1e24a122c..0000000000 --- a/test/utils/dns-fetch-stub.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -// Create a fetch stub with a fall through to the provided fetch implementation -// if the URL doesn't match https://ipfs.io/api/v0/dns?arg=ipfs.io. -module.exports = (fetch) => { - return function () { - if (arguments[0].startsWith('https://ipfs.io/api/v0/dns?arg=ipfs.io')) { - return Promise.resolve({ - json: () => Promise.resolve({ - Path: '/ipfs/QmYNQJoKGNHTpPxCBPh9KkDpaExgd2duMa3aF6ytMpHdao' - }) - }) - } - return fetch.apply(this, arguments) - } -}