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 3 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"is-stream": "^1.1.0",
"joi": "^13.1.2",
"joi-browser": "^13.0.1",
"joi-ipfs-config": "^1.0.2",
"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)
4 changes: 2 additions & 2 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const CID = require('cids')
const debug = require('debug')
const extend = require('deep-extend')
const EventEmitter = require('events')
const Joi = require('joi').extend(require('joi-ipfs-config'))

const Config = require('./config')
Copy link
Member

Choose a reason for hiding this comment

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

s/Config/config

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

options = Joi.attempt(options || {}, Joi.ipfsConfig())
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 config = {}
expect(() => Config.validate(config)).to.not.throw()
})

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

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

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

it('should validate valid config', () => {
const configs = [
{ 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 }
]

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

it('should validate invalid config', () => {
const configs = [
{ 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 }
]

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.not.throw())
})

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

configs.forEach(c => expect(() => Config.validate(c)).to.throw())
})
})