node-etcd
Version:
etcd library for node.js (etcd v2 api)
444 lines (349 loc) • 14.5 kB
text/coffeescript
should = require 'should'
nock = require 'nock'
simple = require 'simple-mock'
Etcd = require '../src/index.coffee'
# Set env var to skip timeouts
process.env.RUNNING_UNIT_TESTS = true
# Helpers
getNock = (host = 'http://127.0.0.1:2379') ->
nock host
beforeEach () ->
nock.cleanAll()
# Tests for utility functions
describe 'Utility', ->
etcd = new Etcd
describe '#_stripSlashPrefix()', ->
it 'should strip prefix-/ from key', ->
etcd._stripSlashPrefix("/key/").should.equal("key/")
etcd._stripSlashPrefix("key/").should.equal("key/")
describe '#_prepareOpts()', ->
it 'should return default request options', ->
etcd._prepareOpts('keypath/key').should.containEql {
json: true
path: '/v2/keypath/key'
}
describe 'Connecting', ->
mock = (host = 'http://127.0.0.1:2379/') ->
nock(host)
.get('/v2/keys/key')
.reply(200, '{"action":"GET","key":"/key","value":"value","index":1}')
it 'should support empty constructor (localhost:2379)', (done) ->
etcd = new Etcd
m = mock()
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
it 'should support string as connect host', (done) ->
etcd = new Etcd "testhost.com:4009"
m = mock("http://testhost.com:4009")
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
it 'should support string prefixed by http:// as host', (done) ->
etcd = new Etcd "http://testhost.com:4009"
m = mock("http://testhost.com:4009")
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
it 'should support string postfixed by / as host', (done) ->
etcd = new Etcd "http://testhost.com:4009/"
m = mock("http://testhost.com:4009")
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
it 'should support array of strings as host', (done) ->
etcd = new Etcd ["http://testhost.com:4009"]
m = mock("http://testhost.com:4009")
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
it 'should support https strings', (done) ->
etcd = new Etcd ["https://testhost.com:1000"]
m = mock("https://testhost.com:1000")
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
describe 'Basic auth', ->
it 'should support basic auth', (done) ->
auth =
user: "username"
pass: "password"
etcd = new Etcd "localhost:2379", { auth: auth }
m = nock("http://localhost:2379")
.get("/v2/keys/key")
.basicAuth(
user: "username",
pass: "password"
)
.reply(200)
etcd.get 'key', (err, val) ->
m.isDone().should.be.true
done err, val
describe 'Basic functions', ->
etcd = new Etcd
checkVal = (done) ->
(err, val) ->
val.should.containEql { value: "value" }
done err, val
describe '#get()', ->
it 'should return entry from etcd', (done) ->
getNock()
.get('/v2/keys/key')
.reply(200, '{"action":"GET","key":"/key","value":"value","index":1}')
etcd.get 'key', checkVal done
it 'should send options to etcd as request url', (done) ->
getNock()
.get('/v2/keys/key?recursive=true')
.reply(200, '{"action":"GET","key":"/key","value":"value","index":1}')
etcd.get 'key', { recursive: true }, checkVal done
it 'should callback with error on error', (done) ->
getNock()
.get('/v2/keys/key')
.reply(404, {"errorCode": 100, "message": "Key not found"})
etcd.get 'key', (err, val) ->
err.should.be.instanceOf Error
err.error.errorCode.should.equal 100
err.message.should.equal "Key not found"
done()
describe '#getSync()', ->
it 'should synchronously return entry from etcd', (done) ->
getNock()
.get('/v2/keys/key')
.reply(200, '{"action":"GET","key":"/key","value":"value","index":1}')
val = etcd.getSync 'key'
doneCheck = checkVal done
doneCheck val.err, val.body
it 'should synchronously return with error on error', (done) ->
getNock()
.get('/v2/keys/key')
.reply(404, {"errorCode": 100, "message": "Key not found"})
val = etcd.getSync 'key'
val.err.should.be.instanceOf Error
val.err.error.errorCode.should.equal 100
val.err.message.should.equal "Key not found"
done()
describe '#set()', ->
it 'should put to etcd', (done) ->
getNock()
.put('/v2/keys/key', { value: "value" })
.reply(200, '{"action":"SET","key":"/key","prevValue":"value","value":"value","index":1}')
etcd.set 'key', 'value', checkVal done
it 'should send options to etcd as request url', (done) ->
getNock()
.put('/v2/keys/key?prevValue=oldvalue', { value: "value"})
.reply(200, '{"action":"SET","key":"/key","prevValue":"oldvalue","value":"value","index":1}')
etcd.set 'key', 'value', { prevValue: "oldvalue" }, checkVal done
it 'should follow 307 redirects', (done) ->
(nock 'http://127.0.0.1:4002')
.put('/v2/keys/key', { value: "value" })
.reply(200, '{"action":"SET","key":"/key","prevValue":"value","value":"value","index":1}')
(nock 'http://127.0.0.1:2379')
.put('/v2/keys/key', { value: "value" })
.reply(307, "", { location: "http://127.0.0.1:4002/v2/keys/key" })
etcd.set 'key', 'value', checkVal done
describe '#setSync()', ->
it 'should synchronously put to etcd', (done) ->
getNock()
.put('/v2/keys/key', { value: "value" })
.reply(200, '{"action":"SET","key":"/key","prevValue":"value","value":"value","index":1}')
val = etcd.setSync 'key', 'value'
doneCheck = checkVal done
doneCheck val.err, val.body
describe '#create()', ->
it 'should post value to dir', (done) ->
getNock()
.post('/v2/keys/dir', { value: "value" })
.reply(200, '{"action":"create", "node":{"key":"/dir/2"}}')
etcd.create 'dir', 'value', (err, val) ->
val.should.containEql { action: "create" }
done err, val
describe '#post()', ->
it 'should post value to key', (done) ->
getNock().post('/v2/keys/key', { value: "value" }).reply(200)
etcd.post 'key', 'value', done
describe '#compareAndSwap()', ->
it 'should set using prevValue', (done) ->
getNock()
.put('/v2/keys/key?prevValue=oldvalue', { value: "value"})
.reply(200, '{"action":"SET","key":"/key","prevValue":"oldvalue","value":"value","index":1}')
etcd.compareAndSwap 'key', 'value', 'oldvalue', checkVal done
it 'has alias testAndSet', ->
etcd.testAndSet.should.equal etcd.testAndSet
describe '#compareAndDelete', ->
it 'should delete using prevValue', (done) ->
getNock().delete('/v2/keys/key?prevValue=oldvalue').reply(200)
etcd.compareAndDelete 'key', 'oldvalue', done
it 'has alias testAndDelete', ->
etcd.compareAndDelete.should.equal etcd.testAndDelete
describe '#mkdir()', ->
it 'should create directory', (done) ->
getNock()
.put('/v2/keys/key?dir=true')
.reply(200, '{"action":"create","node":{"key":"/key","dir":true,"modifiedIndex":1,"createdIndex":1}}')
etcd.mkdir 'key', (err, val) ->
val.should.containEql {action: "create"}
val.node.should.containEql {key: "/key"}
val.node.should.containEql {dir: true}
done()
it 'should work when no options or callback given - bug #56', (done) ->
replybody = '{"action":"create", "node":{"key":"/key","dir":true,"modifiedIndex":1,"createdIndex":1}}'
getNock()
.put('/v2/keys/key?dir=true')
.reply(200, (uri, req, cb) ->
cb(replybody)
done()
)
etcd.mkdir 'key'
describe '#mkdirSync()', ->
it 'should synchronously create directory', (done) ->
getNock()
.put('/v2/keys/key?dir=true')
.reply(200, '{"action":"create","node":{"key":"/key","dir":true,"modifiedIndex":1,"createdIndex":1}}')
val = etcd.mkdirSync 'key'
val.body.should.containEql {action: "create"}
val.body.node.should.containEql {key: "/key"}
val.body.node.should.containEql {dir: true}
done()
describe '#rmdir()', ->
it 'should remove directory', (done) ->
getNock().delete('/v2/keys/key?dir=true').reply(200)
etcd.rmdir 'key', done
describe '#rmdirSync()', ->
it 'should synchronously remove directory', (done) ->
getNock().delete('/v2/keys/key?dir=true')
.reply(200, '{"action":"delete","node":{"key":"/key","dir":true,"modifiedIndex":1,"createdIndex":3}}')
val = etcd.rmdirSync 'key'
val.body.should.containEql {action: "delete"}
val.body.node.should.containEql {dir: true}
done()
describe '#del()', ->
it 'should delete a given key in etcd', (done) ->
getNock().delete('/v2/keys/key').reply(200)
etcd.del 'key', done
describe '#delSync()', ->
it 'should synchronously delete a given key in etcd', (done) ->
getNock().delete('/v2/keys/key2').reply(200, '{"action":"delete"}')
val = etcd.delSync 'key2'
val.body.should.containEql {action: "delete"}
done()
describe '#watch()', ->
it 'should do a get with wait=true', (done) ->
getNock()
.get('/v2/keys/key?wait=true')
.reply(200, '{"action":"set","key":"/key","value":"value","modifiedIndex":7}')
etcd.watch 'key', checkVal done
describe '#watchIndex()', ->
it 'should do a get with wait=true and waitIndex=x', (done) ->
getNock()
.get('/v2/keys/key?waitIndex=1&wait=true')
.reply(200, '{"action":"set","key":"/key","value":"value","modifiedIndex":7}')
etcd.watchIndex 'key', 1, checkVal done
describe '#raw()', ->
it 'should use provided method', (done) ->
getNock().patch('/key').reply(200, 'ok')
etcd.raw 'PATCH', 'key', null, {}, done
it 'should send provided value', (done) ->
getNock().post('/key', { value: "value" }).reply(200, 'ok')
etcd.raw 'POST', 'key', "value", {}, done
it 'should call cb on value from etcd', (done) ->
getNock().get('/key').reply(200, 'value')
etcd.raw 'GET', 'key', null, {}, (err, val) ->
val.should.equal 'value'
done err, val
describe '#machines()', ->
it 'should ask etcd for connected machines', (done) ->
getNock().get('/v2/keys/_etcd/machines').reply(200, '{"value":"value"}')
etcd.machines checkVal done
describe '#leader()', ->
it 'should ask etcd for leader', (done) ->
getNock().get('/v2/leader').reply(200, '{"value":"value"}')
etcd.leader checkVal done
describe '#leaderStats()', ->
it 'should ask etcd for statistics for leader', (done) ->
getNock().get('/v2/stats/leader').reply(200, '{"value":"value"}')
etcd.leaderStats checkVal done
describe '#selfStats()', ->
it 'should ask etcd for statistics for connected server', (done) ->
getNock().get('/v2/stats/self').reply(200, '{"value":"value"}')
etcd.selfStats checkVal done
describe '#version()', ->
it 'should ask etcd for version', (done) ->
getNock().get('/version').reply(200, 'etcd v0.1.0-8-gaad1626')
etcd.version (err, val) ->
val.should.equal 'etcd v0.1.0-8-gaad1626'
done err, val
describe 'SSL support', ->
beforeEach () ->
nock.cleanAll()
it 'passes ssl options to request lib', (done) ->
etcdssl = new Etcd 'https://localhost:4009', {ca: 'myca', cert: 'mycert', key: 'mykey'}
simple.mock(etcdssl.client, "_doRequest").callFn (options) ->
options.should.containEql
ca: 'myca'
cert: 'mycert'
key: 'mykey'
done()
etcdssl.get 'key'
describe 'Cancellation Token', ->
beforeEach () ->
nock.cleanAll()
it 'should return token on request', ->
getNock().get('/version').reply(200, 'etcd v0.1.0-8-gaad1626')
etcd = new Etcd
token = etcd.version()
token.abort.should.be.a.function
token.isAborted().should.be.false
it 'should stop execution on abort', (done) ->
http = getNock()
.log(console.log)
.get('/v2/keys/key')
.reply(200, '{"action":"GET","key":"/key","value":"value","index":1}')
etcd = new Etcd '127.0.0.1', 2379
# This sucks a bit.. are there any better way of checking that a callback
# does not happen?
token = etcd.get "key", () -> throw new Error "Call should have been aborted"
token.abort()
setTimeout done, 50
describe 'Multiserver/Cluster support', ->
beforeEach () ->
nock.cleanAll()
it 'should accept list of servers in constructor', ->
etcd = new Etcd ['localhost:2379', 'localhost:4002']
etcd.getHosts().should.eql ['http://localhost:2379', 'http://localhost:4002']
it 'should try next server in list on http error', (done) ->
path = '/v2/keys/foo'
response = '{"action":"GET","key":"/key","value":"value","index":1}'
handler = (uri) ->
nock.cleanAll()
getNock('http://s1').get(path).reply(200, response)
getNock('http://s2').get(path).reply(200, response)
return {}
getNock('http://s1').get(path).reply(500, handler)
getNock('http://s2').get(path).reply(500, handler)
etcd = new Etcd ['s1', 's2']
etcd.get 'foo', (err, res) ->
res.value.should.eql 'value'
done()
it 'should callback error if all servers failed', (done) ->
path = '/v2/keys/foo'
getNock('http://s1').get(path).reply(500, {})
getNock('http://s2').get(path).reply(500, {})
etcd = new Etcd ['s1', 's2']
etcd.get 'foo', (err, res) ->
err.should.be.an.instanceOf Error
err.errors.should.have.lengthOf 2
done()
describe 'when cluster is doing leader elect', () ->
it 'should retry on connection refused', (done) ->
etcd = new Etcd ("localhost:#{p}" for p in [47187, 47188, 47189])
token = etcd.set 'a', 'b', (err) ->
err.errors.length.should.be.exactly 12
token.errors.length.should.be.exactly 12
token.retries.should.be.exactly 3
done()
it 'should allow maxRetries to control number of retries', (done) ->
etcd = new Etcd ("localhost:#{p}" for p in [47187, 47188, 47189])
token = etcd.set 'a', 'b', { maxRetries: 1 }, (err) ->
err.errors.length.should.be.exactly 6
token.retries.should.be.exactly 1
done()