diff --git a/package.json b/package.json index ce0696fc34..87174bb503 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "ipfs-http-response": "^0.5.0", "ipfs-mfs": "^1.0.0", "ipfs-multipart": "^0.3.0", - "ipfs-repo": "^0.30.0", + "ipfs-repo": "^1.0.0", "ipfs-unixfs": "^0.3.0", "ipfs-unixfs-exporter": "^0.41.0", "ipfs-unixfs-importer": "^0.44.0", @@ -155,7 +155,6 @@ "multicodec": "^1.0.0", "multihashes": "~0.4.14", "multihashing-async": "^0.8.0", - "p-defer": "^3.0.0", "p-queue": "^6.1.0", "parse-duration": "^0.1.2", "peer-id": "^0.13.5", diff --git a/src/core/api-manager.js b/src/core/api-manager.js index 5ccc055e98..1fb8542121 100644 --- a/src/core/api-manager.js +++ b/src/core/api-manager.js @@ -1,23 +1,128 @@ 'use strict' +const noop = () => {} +const defaultApi = (onUndef = noop) => ({ + add: onUndef, + bitswap: { + stat: onUndef, + unwant: onUndef, + wantlist: onUndef + }, + block: { + get: onUndef, + put: onUndef, + rm: onUndef, + stat: onUndef + }, + bootstrap: { + add: onUndef, + list: onUndef, + rm: onUndef + }, + cat: onUndef, + config: onUndef, + dag: { + get: onUndef, + put: onUndef, + resolve: onUndef, + tree: onUndef + }, + dns: onUndef, + files: { + chmod: onUndef, + cp: onUndef, + flush: onUndef, + ls: onUndef, + mkdir: onUndef, + mv: onUndef, + read: onUndef, + rm: onUndef, + stat: onUndef, + touch: onUndef, + write: onUndef + }, + get: onUndef, + id: onUndef, + init: onUndef, + isOnline: onUndef, + key: { + export: onUndef, + gen: onUndef, + import: onUndef, + info: onUndef, + list: onUndef, + rename: onUndef, + rm: onUndef + }, + ls: onUndef, + name: { + publish: onUndef, + pubsub: { + cancel: onUndef, + state: onUndef, + subs: onUndef + } + }, + object: { + data: onUndef, + get: onUndef, + links: onUndef, + new: onUndef, + patch: { + addLink: onUndef, + appendData: onUndef, + rmLink: onUndef, + setData: onUndef + }, + put: onUndef, + stat: onUndef + }, + pin: onUndef, + ping: onUndef, + pubsub: { + subscribe: onUndef, + unsubscribe: onUndef, + publish: onUndef, + ls: onUndef, + peers: onUndef + }, + refs: onUndef, + repo: { + gc: onUndef, + stat: onUndef, + version: onUndef + }, + resolve: onUndef, + start: onUndef, + stats: { + bitswap: onUndef, + bw: onUndef, + repo: onUndef + }, + stop: onUndef, + swarm: { + addrs: onUndef, + connect: onUndef, + disconnect: onUndef, + localAddrs: onUndef, + peers: onUndef + }, + version: onUndef +}) + module.exports = class ApiManager { constructor () { - this._api = {} - this._onUndef = () => undefined - this.api = new Proxy(this._api, { - get: (_, prop) => { - if (prop === 'then') return undefined // Not a promise! - return this._api[prop] === undefined ? this._onUndef(prop) : this._api[prop] - } - }) + this.api = { + ...defaultApi() + } } update (nextApi, onUndef) { const prevApi = { ...this._api } const prevUndef = this._onUndef - Object.keys(this._api).forEach(k => { delete this._api[k] }) - Object.assign(this._api, nextApi) - if (onUndef) this._onUndef = onUndef + Object.keys(this.api).forEach(k => { delete this.api[k] }) + Object.assign(this.api, defaultApi(onUndef), nextApi) + this._onUndef = onUndef || noop return { cancel: () => this.update(prevApi, prevUndef), api: this.api } } } diff --git a/src/core/components/init.js b/src/core/components/init.js index 7e2f0ce94d..5b4b587e44 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -5,7 +5,6 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const mergeOptions = require('merge-options') const getDefaultConfig = require('../runtime/config-nodejs.js') -const createRepo = require('../runtime/repo-nodejs') const Keychain = require('libp2p-keychain') const NoKeychain = require('./no-keychain') const mortice = require('mortice') @@ -13,16 +12,14 @@ const { DAGNode } = require('ipld-dag-pb') const UnixFs = require('ipfs-unixfs') const multicodec = require('multicodec') const { - AlreadyInitializingError, AlreadyInitializedError, - NotStartedError, - NotEnabledError + AlreadyInitializingError, + NotStartedError } = require('../errors') const BlockService = require('ipfs-block-service') const Ipld = require('ipld') const getDefaultIpldOptions = require('../runtime/ipld-nodejs') const createPreloader = require('../preload') -const { ERR_REPO_NOT_INITIALIZED } = require('ipfs-repo').errors const IPNS = require('../ipns') const OfflineDatastore = require('../ipns/routing/offline-datastore') const initAssets = require('../runtime/init-assets-nodejs') @@ -32,9 +29,14 @@ const Components = require('./') module.exports = ({ apiManager, print, - options: constructorOptions + options: constructorOptions, + repo }) => async function init (options) { - const { cancel } = apiManager.update({ init: () => { throw new AlreadyInitializingError() } }) + const { cancel } = apiManager.update({ + init: () => { + throw new AlreadyInitializingError() + } + }) try { options = options || {} @@ -49,30 +51,9 @@ module.exports = ({ options.config = mergeOptions(options.config, constructorOptions.config) } - options.repo = options.repo || constructorOptions.repo options.repoAutoMigrate = options.repoAutoMigrate || constructorOptions.repoAutoMigrate - const repo = typeof options.repo === 'string' || options.repo == null - ? createRepo({ path: options.repo, autoMigrate: options.repoAutoMigrate }) - : options.repo - - let isInitialized = true - - if (repo.closed) { - try { - await repo.open() - } catch (err) { - if (err.code === ERR_REPO_NOT_INITIALIZED) { - isInitialized = false - } else { - throw err - } - } - } - - if (!isInitialized && options.allowNew === false) { - throw new NotEnabledError('new repo initialization is not enabled') - } + const isInitialized = await repo.isInitialized() const { peerId, keychain } = isInitialized ? await initExistingRepo(repo, options) @@ -212,6 +193,7 @@ async function initNewRepo (repo, { privateKey, emptyRepo, bits, profiles, confi } async function initExistingRepo (repo, { config: newConfig, profiles, pass }) { + await repo.open() let config = await repo.config.get() if (newConfig || profiles) { diff --git a/src/core/components/start.js b/src/core/components/start.js index ec771cb09b..1edade5eae 100644 --- a/src/core/components/start.js +++ b/src/core/components/start.js @@ -3,10 +3,14 @@ const Bitswap = require('ipfs-bitswap') const multiaddr = require('multiaddr') const get = require('dlv') -const defer = require('p-defer') const IPNS = require('../ipns') const routingConfig = require('../ipns/routing/config') -const { AlreadyInitializedError, NotEnabledError } = require('../errors') +const { + AlreadyInitializedError, + AlreadyStartingError, + AlreadyStartedError, + NotEnabledError +} = require('../errors') const Components = require('./') const createMfsPreload = require('../mfs-preload') @@ -24,8 +28,11 @@ module.exports = ({ print, repo }) => async function start () { - const startPromise = defer() - const { cancel } = apiManager.update({ start: () => startPromise.promise }) + const { cancel } = apiManager.update({ + start: () => { + throw new AlreadyStartingError() + } + }) try { // The repo may be closed if previously stopped @@ -97,14 +104,12 @@ module.exports = ({ repo }) - apiManager.update(api, () => undefined) + apiManager.update(api) } catch (err) { cancel() - startPromise.reject(err) throw err } - startPromise.resolve(apiManager.api) return apiManager.api } @@ -234,7 +239,7 @@ function createApi ({ version: Components.repo.version({ repo }) }, resolve, - start: () => apiManager.api, + start: async () => { throw new AlreadyStartedError() }, // eslint-disable-line require-await stats: { bitswap: Components.bitswap.stat({ bitswap }), bw: libp2p.metrics diff --git a/src/core/components/stop.js b/src/core/components/stop.js index f8ff775199..67a52ac439 100644 --- a/src/core/components/stop.js +++ b/src/core/components/stop.js @@ -1,7 +1,10 @@ 'use strict' -const defer = require('p-defer') -const { NotStartedError, AlreadyInitializedError } = require('../errors') +const { + NotStartedError, + AlreadyInitializedError, + AlreadyStoppingError +} = require('../errors') const Components = require('./') module.exports = ({ @@ -22,8 +25,11 @@ module.exports = ({ print, repo }) => async function stop () { - const stopPromise = defer() - const { cancel } = apiManager.update({ stop: () => stopPromise.promise }) + const { cancel } = apiManager.update({ + stop: () => { + throw new AlreadyStoppingError() + } + }) try { blockService.unsetExchange() @@ -58,11 +64,9 @@ module.exports = ({ apiManager.update(api, () => { throw new NotStartedError() }) } catch (err) { cancel() - stopPromise.reject(err) throw err } - stopPromise.resolve(apiManager.api) return apiManager.api } @@ -182,7 +186,7 @@ function createApi ({ bw: notStarted, repo: Components.repo.stat({ repo }) }, - stop: () => apiManager.api, + stop: notStarted, swarm: { addrs: notStarted, connect: notStarted, diff --git a/src/core/errors.js b/src/core/errors.js index 465576b322..d1272fb6d3 100644 --- a/src/core/errors.js +++ b/src/core/errors.js @@ -44,6 +44,39 @@ class NotStartedError extends Error { NotStartedError.code = 'ERR_NOT_STARTED' exports.NotStartedError = NotStartedError +class AlreadyStartingError extends Error { + constructor (message = 'already starting') { + super(message) + this.name = 'AlreadyStartingError' + this.code = AlreadyStartingError.code + } +} + +AlreadyStartingError.code = 'ERR_ALREADY_STARTING' +exports.AlreadyStartingError = AlreadyStartingError + +class AlreadyStartedError extends Error { + constructor (message = 'already started') { + super(message) + this.name = 'AlreadyStartedError' + this.code = AlreadyStartedError.code + } +} + +AlreadyStartedError.code = 'ERR_ALREADY_STARTED' +exports.AlreadyStartedError = AlreadyStartedError + +class AlreadyStoppingError extends Error { + constructor (message = 'already started') { + super(message) + this.name = 'AlreadyStoppingError' + this.code = AlreadyStartedError.code + } +} + +AlreadyStoppingError.code = 'ERR_ALREADY_STOPPING' +exports.AlreadyStoppingError = AlreadyStoppingError + class NotEnabledError extends Error { constructor (message = 'not enabled') { super(message) diff --git a/src/core/index.js b/src/core/index.js index 4e748d7ed8..348b63f6da 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -16,9 +16,10 @@ const multibase = require('multibase') const multicodec = require('multicodec') const multihashing = require('multihashing-async') const CID = require('cids') -const { NotInitializedError } = require('./errors') +const { NotInitializedError, NotEnabledError } = require('./errors') const Components = require('./components') const ApiManager = require('./api-manager') +const createRepo = require('./runtime/repo-nodejs') const getDefaultOptions = () => ({ init: true, @@ -30,7 +31,8 @@ const getDefaultOptions = () => ({ '/dns4/node0.preload.ipfs.io/https', '/dns4/node1.preload.ipfs.io/https' ] - } + }, + repoAutoMigrate: false }) async function create (options) { @@ -38,26 +40,39 @@ async function create (options) { // eslint-disable-next-line no-console const print = options.silent ? log : console.log + const repo = typeof options.repo === 'string' || options.repo == null + ? createRepo({ path: options.repo, autoMigrate: options.repoAutoMigrate }) + : options.repo const apiManager = new ApiManager() - const { api } = apiManager.update({ - init: Components.init({ apiManager, print, options }), + init: Components.init({ + apiManager, + print, + options, + repo + }), dns: Components.dns(), isOnline: Components.isOnline({}) }, async () => { throw new NotInitializedError() }) // eslint-disable-line require-await - if (!options.init) { - return api + if (await repo.isInitialized()) { + // FIXME: the repo is already initialised so we are only calling init + // here for the side effect of it updating the available api operations + await api.init() + } else if (options.init.allowNew === false) { + throw new NotEnabledError('new repo initialization is not enabled') } - await api.init() + if (options.init && !(await repo.isInitialized())) { + await api.init() + } - if (!options.start) { - return api + if (options.start) { + return api.start() } - return api.start() + return api } module.exports = { diff --git a/test/core/bitswap.spec.js b/test/core/bitswap.spec.js index d772eb8dff..0c8cd9cbc6 100644 --- a/test/core/bitswap.spec.js +++ b/test/core/bitswap.spec.js @@ -22,6 +22,8 @@ describe('bitswap', function () { this.timeout(60 * 1000) const df = factory() + afterEach(() => df.clean()) + describe('transfer a block between', () => { it('2 peers', async function () { const remote = (await df.spawn({ type: 'js' })).api @@ -33,7 +35,6 @@ describe('bitswap', function () { const b = await remote.block.get(block.cid) expect(b.data).to.eql(block.data) - await df.clean() }) it('3 peers', async () => { @@ -57,7 +58,6 @@ describe('bitswap', function () { expect(await remote2.block.get(block.cid)).to.eql(block) expect(await proc.block.get(block.cid)).to.eql(block) }, { concurrency: 3 }) - await df.clean() }) }) @@ -66,6 +66,7 @@ describe('bitswap', function () { // TODO make this test more interesting (10Mb file) // TODO remove randomness from the test const file = Buffer.from(`I love IPFS <3 ${hat()}`) + const remote = (await df.spawn({ type: 'js' })).api const proc = (await df.spawn({ type: 'proc' })).api await proc.swarm.connect(remote.peerId.addresses[0]) @@ -73,7 +74,6 @@ describe('bitswap', function () { const files = await all(remote.add([{ path: 'awesome.txt', content: file }])) const data = await concat(proc.cat(files[0].cid)) expect(data.slice()).to.eql(file) - await df.clean() }) }) @@ -85,8 +85,6 @@ describe('bitswap', function () { } catch (err) { expect(err).to.exist() expect(err.code).to.equal('ERR_INVALID_CID') - } finally { - await df.clean() } }) }) diff --git a/test/core/create-node.spec.js b/test/core/create-node.spec.js index 8b63ee1ae0..8e8ac47e62 100644 --- a/test/core/create-node.spec.js +++ b/test/core/create-node.spec.js @@ -168,6 +168,10 @@ describe('create node', function () { repo: tempRepo, init: { bits: 512 }, config: { + Addresses: { + Swarm: [] + }, + Bootstrap: [], Pubsub: { Enabled: false } diff --git a/test/core/init.spec.js b/test/core/init.spec.js index 2089e4a02f..bcdc807453 100644 --- a/test/core/init.spec.js +++ b/test/core/init.spec.js @@ -6,6 +6,10 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha') const { isNode } = require('ipfs-utils/src/env') const hat = require('hat') const IPFS = require('../../src/core') +const { + AlreadyInitializedError, + AlreadyInitializingError +} = require('../../src/core/errors') const privateKey = 'CAASqAkwggSkAgEAAoIBAQChVmiObYo6pkKrMSd3OzW1cTL+RDmX1rkETYGKWV9TPXMNgElFTYoYHqT9QZomj5RI8iUmHccjzqr4J0mV+E0NpvHHOLlmDZ82lAw2Zx7saUkeQWvC0S9Z0o3aTx2sSubZV53rSomkZgQH4fYTs4RERejV4ltzLFdzQQBwWrBvlagpPHUCxKDUCnE5oIzdbD26ltWViPBWr7TfotzC8Lyi/tceqCpHMUJGMbsVgypnlgpey07MBvs71dVh5LcRen/ztsQO6Yju4D3QgWoyD0SIUdJFvBzEwL9bSiA3QjUc/fkGd7EcdN5bebYOqAi4ZIiAMLp3i4+B8Tzq/acull43AgMBAAECggEBAIDgZE75o4SsEO9tKWht7L5OeXxxBUyMImkUfJkGQUZd/MzZIC5y/Q+9UvBW+gs5gCsw+onTGaM50Iq/32Ej4nE4XURVxIuH8BmJ86N1hlc010qK2cjajqeCsPulXT+m6XbOLYCpnv+q2idt0cL1EH/1FEPeOEztK8ION4qIdw36SoykfTx/RqtkKHtS01AwN82EOPbWk7huyQT5R5MsCZmRJXBFkpNtiL+8619BH2aVlghHO4NouF9wQjdz/ysVuyYg+3rX2cpGjuHDTZ6hVQiJD1lF6D+dua7UPyHYAG2iRQiKZmCjitt9ywzPxiRaYF/aZ02FEMWckZulR09axskCgYEAzjl6ER8WwxYHn4tHse+CrIIF2z5cscdrh7KSwd3Rse9hIIBDJ/0KkvoYd1IcWrS8ywLrRfSLIjEU9u7IN1m+IRVWJ61fXNqOHm9clAu6qNhCN6W2+JfxDkUygTwmsq0v3huO+qkiMQz+a4nAXJe8Utd36ywgPhVGxFa/7x1v1N0CgYEAyEdiYRFf1aQZcO7+B2FH+tkGJsB30VIBhcpG9EukuQUUulLHhScc/KRj+EFAACLdkTqlVI0xVYIWaaCXwoQCWKixjZ5mYPC+bBLgn4IoDS6XTdHtR7Vn3UUvGTKsM0/z4e8/0eSzGNCHoYez9IoBlPNic0sQuST4jzgS2RYnFCMCgYASWSzSLyjwTJp7CIJlg4Dl5l+tBRxsOOkJVssV8q2AnmLO6HqRKUNylkvs+eJJ88DEc0sJm1txvFo4KkCoJBT1jpduyk8szMlOTew3w99kvHEP0G+6KJKrCV8X/okW5q/WnC8ZgEjpglV0rfnugxWfbUpfIzrvKydzuqAzHzRfBQKBgQDANtKSeoxRjEbmfljLWHAure8bbgkQmfXgI7xpZdfXwqqcECpw/pLxXgycDHOSLeQcJ/7Y4RGCEXHVOk2sX+mokW6mjmmPjD4VlyCBtfcef6KzC1EBS3c9g9KqCln+fTOBmY7UsPu6SxiAzK7HeVP/Un8gS+Dm8DalrZlZQ8uJpQKBgF6mL/Xo/XUOiz2jAD18l8Y6s49bA9H2CoLpBGTV1LfY5yTFxRy4R3qnX/IzsKy567sbtkEFKJxplc/RzCQfrgbdj7k26SbKtHR3yERaFGRYq8UeAHeYC1/N19LF5BMQL4y5R4PJ1SFPeJCL/wXiMqs1maTqvKqtc4bbegNdwlxn' @@ -25,6 +29,7 @@ describe('init', function () { repo, init: false, start: false, + silent: true, preload: { enabled: false } }) }) @@ -89,4 +94,65 @@ describe('init', function () { expect(config.Bootstrap).to.be.empty() expect(config.Discovery.MDNS.Enabled).to.be.true() }) + + it('should be initialised if initialised previously', async () => { + // init and start a node + ipfs = await IPFS.create({ + repo, + init: true, + start: true, + silent: true, + preload: { enabled: false } + }) + // now stop it + await ipfs.stop() + + // create a new instance with the same repo + ipfs = await IPFS.create({ + repo, + init: false, + start: false, + silent: true, + preload: { enabled: false } + }) + + // should be able to start the node and not have to init it first + await ipfs.start() + await ipfs.stop() + }) + + it('should explode if double init in parallel', async () => { + ipfs = await IPFS.create({ + repo, + init: false, + start: false, + silent: true, + preload: { enabled: false } + }) + + let promise + + await expect(() => { + promise = ipfs.init() + ipfs.init() + }).to.throw(AlreadyInitializingError) + + // wait for the first init promise to resolve - this is because the second + // will cause the test to exit and teardown of the repo to commence which + // pulls the rug out from under the init operation + await promise + }) + + it('should explode if double init in series', async () => { + ipfs = await IPFS.create({ + repo, + init: false, + start: false, + silent: true, + preload: { enabled: false } + }) + + await ipfs.init() + expect(ipfs.init()).to.eventually.be.rejectedWith(AlreadyInitializedError) + }) }) diff --git a/test/core/start.spec.js b/test/core/start.spec.js new file mode 100644 index 0000000000..9d7e5b88d8 --- /dev/null +++ b/test/core/start.spec.js @@ -0,0 +1,113 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const { expect } = require('interface-ipfs-core/src/utils/mocha') +const { isNode } = require('ipfs-utils/src/env') +const IPFS = require('../../src/core') +const { + AlreadyStartingError, + AlreadyStartedError, + NotInitializedError +} = require('../../src/core/errors') + +// This gets replaced by `create-repo-browser.js` in the browser +const createTempRepo = require('../utils/create-repo-nodejs.js') + +describe('start', function () { + if (!isNode) return + + let ipfs + let repo + + beforeEach(() => { + repo = createTempRepo() + }) + + afterEach(async () => { + if (ipfs && ipfs.isOnline()) { + await ipfs.stop() + } + + await repo.teardown() + }) + + it('should start successfully', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: false, + silent: true, + preload: { enabled: false } + }) + + expect(ipfs.isOnline()).to.be.false() + await ipfs.start() + expect(ipfs.isOnline()).to.be.true() + }) + + it('should start and stop and start successfully', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: false, + silent: true, + preload: { enabled: false } + }) + + expect(ipfs.isOnline()).to.be.false() + await ipfs.start() + expect(ipfs.isOnline()).to.be.true() + await ipfs.stop() + expect(ipfs.isOnline()).to.be.false() + await ipfs.start() + expect(ipfs.isOnline()).to.be.true() + }) + + it('should explode when starting a node twice in parallel', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: false, + silent: true, + preload: { enabled: false } + }) + + let promise + + await expect(() => { + promise = ipfs.start() + ipfs.start() + }).to.throw(AlreadyStartingError) + + // wait for the first start promise to resolve - this is because the second + // will cause the test to exit before `ipfs.isOnline` returns true, so the + // node will not be stopped which messes up the rest of the tests + await promise + }) + + it('should explode when starting a node twice in series', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: false, + silent: true, + preload: { enabled: false } + }) + + await ipfs.start() + await expect(ipfs.start()).to.eventually.be.rejectedWith(AlreadyStartedError) + }) + + it('should not start an uninitialised node', async () => { + ipfs = await IPFS.create({ + repo, + init: false, + start: false, + silent: true, + preload: { enabled: false } + }) + + await expect(ipfs.start()).to.eventually.be.rejectedWith(NotInitializedError) + }) +}) diff --git a/test/core/stop.spec.js b/test/core/stop.spec.js new file mode 100644 index 0000000000..d6e5e59992 --- /dev/null +++ b/test/core/stop.spec.js @@ -0,0 +1,100 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const { expect } = require('interface-ipfs-core/src/utils/mocha') +const { isNode } = require('ipfs-utils/src/env') +const IPFS = require('../../src/core') +const { + AlreadyStoppingError, + NotStartedError +} = require('../../src/core/errors') + +// This gets replaced by `create-repo-browser.js` in the browser +const createTempRepo = require('../utils/create-repo-nodejs.js') + +describe('stop', function () { + if (!isNode) return + + let ipfs + let repo + + beforeEach(() => { + repo = createTempRepo() + }) + + afterEach(async () => { + if (ipfs && ipfs.isOnline()) { + await ipfs.stop() + } + + await repo.teardown() + }) + + it('should stop successfully', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: true, + silent: true, + preload: { enabled: false } + }) + + expect(ipfs.isOnline()).to.be.true() + await ipfs.stop() + expect(ipfs.isOnline()).to.be.false() + }) + + it('should start and stop and start and stop successfully', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: true, + silent: true, + preload: { enabled: false } + }) + + expect(ipfs.isOnline()).to.be.true() + await ipfs.stop() + expect(ipfs.isOnline()).to.be.false() + await ipfs.start() + expect(ipfs.isOnline()).to.be.true() + await ipfs.stop() + expect(ipfs.isOnline()).to.be.false() + }) + + it('should explode when stopping a node twice in parallel', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: true, + silent: true, + preload: { enabled: false } + }) + + let promise + + await expect(() => { + promise = ipfs.stop() + ipfs.stop() + }).to.throw(AlreadyStoppingError) + + // wait for the first start promise to resolve - this is because the second + // will cause the test to exit before `ipfs.isOnline` returns true, so the + // node will not be stopped which messes up the rest of the tests + await promise + }) + + it('should explode when starting a node twice in series', async () => { + ipfs = await IPFS.create({ + repo, + init: true, + start: true, + silent: true, + preload: { enabled: false } + }) + + await ipfs.stop() + await expect(ipfs.stop()).to.eventually.be.rejectedWith(NotStartedError) + }) +})