Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

feat: add config validation #1239

Merged
merged 5 commits into from
Mar 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"./src/core/runtime/repo-nodejs.js": "./src/core/runtime/repo-browser.js",
"./src/core/runtime/dns-nodejs.js": "./src/core/runtime/dns-browser.js",
"./test/utils/create-repo-nodejs.js": "./test/utils/create-repo-browser.js",
"stream": "readable-stream"
"stream": "readable-stream",
"joi": "joi-browser"
},
"engines": {
"node": ">=6.0.0",
Expand Down Expand Up @@ -119,6 +120,8 @@
"is-ipfs": "^0.3.2",
"is-stream": "^1.1.0",
"joi": "^13.1.2",
"joi-browser": "^13.0.1",
"joi-multiaddr": "^1.0.1",
"libp2p": "~0.18.0",
"libp2p-circuit": "~0.1.4",
"libp2p-floodsub": "~0.14.1",
Expand Down
43 changes: 43 additions & 0 deletions src/core/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

const Joi = require('joi').extend(require('joi-multiaddr'))

const schema = Joi.object().keys({
repo: Joi.alternatives().try(
Joi.object(), // TODO: schema for IPFS repo
Joi.string()
).allow(null),
init: Joi.alternatives().try(
Joi.boolean(),
Joi.object().keys({ bits: Joi.number().integer() })
).allow(null),
start: Joi.boolean(),
pass: Joi.string().allow(''),
EXPERIMENTAL: Joi.object().keys({
pubsub: Joi.boolean(),
sharding: Joi.boolean(),
dht: Joi.boolean()
}).allow(null),
config: Joi.object().keys({
Addresses: Joi.object().keys({
Swarm: Joi.array().items(Joi.multiaddr().options({ convert: false })),
API: Joi.multiaddr().options({ convert: false }),
Gateway: Joi.multiaddr().options({ convert: false })
}).allow(null),
Discovery: Joi.object().keys({
MDNS: Joi.object().keys({
Enabled: Joi.boolean(),
Interval: Joi.number().integer()
}).allow(null),
webRTCStar: Joi.object().keys({
Enabled: Joi.boolean()
}).allow(null)
}).allow(null),
Bootstrap: Joi.array().items(Joi.multiaddr().IPFS().options({ convert: false }))
}).allow(null),
libp2p: Joi.object().keys({
modules: Joi.object().allow(null) // TODO: schemas for libp2p modules?
}).allow(null)
}).options({ allowUnknown: true })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @alanshaw. Love this. Can you explain why we allow unknown options? Are we trying to be helpful for
developers or open to external config options?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really have a strong opinion either way on this one, I guess it's just to be flexible...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh look, I forgot I already wrote a reason in the description!

The idea is that the validation should be flexible and shouldn't block new features landing in JS-IPFS. Validation of values that are objects allows for unknown keys so that features can land, and validation for the config can be backfilled at a later date if needs be.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


module.exports.validate = (config) => Joi.attempt(config, schema)
3 changes: 2 additions & 1 deletion src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const debug = require('debug')
const extend = require('deep-extend')
const EventEmitter = require('events')

const config = require('./config')
const boot = require('./boot')
const components = require('./components')
// replaced by repo-browser when running in the browser
Expand All @@ -27,7 +28,7 @@ class IPFS extends EventEmitter {
EXPERIMENTAL: {}
}

options = options || {}
options = config.validate(options || {})
this._libp2pModules = options.libp2p && options.libp2p.modules

extend(this._options, options)
Expand Down
220 changes: 220 additions & 0 deletions test/core/config.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/* eslint-env mocha */
'use strict'

const chai = require('chai')
const dirtyChai = require('dirty-chai')
const expect = chai.expect
chai.use(dirtyChai)

const config = require('../../src/core/config')

describe('config', () => {
it('should allow empty config', () => {
const cfg = {}
expect(() => config.validate(cfg)).to.not.throw()
})

it('should allow undefined config', () => {
const cfg = undefined
expect(() => config.validate(cfg)).to.not.throw()
})

it('should allow unknown key at root', () => {
const cfg = { [`${Date.now()}`]: 'test' }
expect(() => config.validate(cfg)).to.not.throw()
})

it('should validate valid repo', () => {
const cfgs = [
{ repo: { unknown: 'value' } },
{ repo: '/path/to-repo' },
{ repo: null },
{ repo: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid repo', () => {
const cfgs = [
{ repo: 138 }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid init', () => {
const cfgs = [
{ init: { bits: 138 } },
{ init: { bits: 138, unknown: 'value' } },
{ init: true },
{ init: false },
{ init: null },
{ init: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid init', () => {
const cfgs = [
{ init: 138 },
{ init: { bits: 'not an int' } }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid start', () => {
const cfgs = [
{ start: true },
{ start: false },
{ start: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid start', () => {
const cfgs = [
{ start: 138 },
{ start: 'make it so number 1' },
{ start: null }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid pass', () => {
const cfgs = [
{ pass: 'correctbatteryhorsestaple' },
{ pass: '' },
{ pass: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid pass', () => {
const cfgs = [
{ pass: 138 },
{ pass: null }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid EXPERIMENTAL', () => {
const cfgs = [
{ EXPERIMENTAL: { pubsub: true, dht: true, sharding: true } },
{ EXPERIMENTAL: { pubsub: false, dht: false, sharding: false } },
{ EXPERIMENTAL: { unknown: 'value' } },
{ EXPERIMENTAL: null },
{ EXPERIMENTAL: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid EXPERIMENTAL', () => {
const cfgs = [
{ EXPERIMENTAL: { pubsub: 138 } },
{ EXPERIMENTAL: { dht: 138 } },
{ EXPERIMENTAL: { sharding: 138 } }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid config', () => {
const cfgs = [
{ config: { Addresses: { Swarm: ['/ip4/0.0.0.0/tcp/4002'] } } },
{ config: { Addresses: { Swarm: [] } } },
{ config: { Addresses: { Swarm: undefined } } },

{ config: { Addresses: { API: '/ip4/127.0.0.1/tcp/5002' } } },
{ config: { Addresses: { API: undefined } } },

{ config: { Addresses: { Gateway: '/ip4/127.0.0.1/tcp/9090' } } },
{ config: { Addresses: { Gateway: undefined } } },

{ config: { Addresses: { unknown: 'value' } } },
{ config: { Addresses: null } },
{ config: { Addresses: undefined } },

{ config: { Discovery: { MDNS: { Enabled: true } } } },
{ config: { Discovery: { MDNS: { Enabled: false } } } },
{ config: { Discovery: { MDNS: { Interval: 138 } } } },
{ config: { Discovery: { MDNS: { unknown: 'value' } } } },
{ config: { Discovery: { MDNS: null } } },
{ config: { Discovery: { MDNS: undefined } } },

{ config: { Discovery: { webRTCStar: { Enabled: true } } } },
{ config: { Discovery: { webRTCStar: { Enabled: false } } } },
{ config: { Discovery: { webRTCStar: { unknown: 'value' } } } },
{ config: { Discovery: { webRTCStar: null } } },
{ config: { Discovery: { webRTCStar: undefined } } },

{ config: { Discovery: { unknown: 'value' } } },
{ config: { Discovery: null } },
{ config: { Discovery: undefined } },

{ config: { Bootstrap: ['/ip4/104.236.176.52/tcp/4001/ipfs/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z'] } },
{ config: { Bootstrap: [] } },

{ config: { unknown: 'value' } },
{ config: null },
{ config: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid config', () => {
const cfgs = [
{ config: { Addresses: { Swarm: 138 } } },
{ config: { Addresses: { Swarm: null } } },

{ config: { Addresses: { API: 138 } } },
{ config: { Addresses: { API: null } } },

{ config: { Addresses: { Gateway: 138 } } },
{ config: { Addresses: { Gateway: null } } },

{ config: { Discovery: { MDNS: { Enabled: 138 } } } },
{ config: { Discovery: { MDNS: { Interval: true } } } },

{ config: { Discovery: { webRTCStar: { Enabled: 138 } } } },

{ config: { Bootstrap: ['/ip4/0.0.0.0/tcp/4002'] } },
{ config: { Bootstrap: 138 } },

{ config: 138 }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})

it('should validate valid libp2p', () => {
const cfgs = [
{ libp2p: { modules: {} } },
{ libp2p: { modules: { unknown: 'value' } } },
{ libp2p: { modules: null } },
{ libp2p: { modules: undefined } },
{ libp2p: { unknown: 'value' } },
{ libp2p: null },
{ libp2p: undefined }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw())
})

it('should validate invalid libp2p', () => {
const cfgs = [
{ libp2p: { modules: 138 } },
{ libp2p: 138 }
]

cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw())
})
})