require 'spec_helper' describe Etcdv3 do let(:lease_stub) { local_stub(Etcdv3::Lease, 1) } context 'Insecure connection without Auth' do let(:conn) { local_connection } describe '#initialize' do context 'without auth' do subject { conn } it { is_expected.to have_attributes(token: nil) } it { is_expected.to have_attributes(user: nil) } it { is_expected.to have_attributes(password: nil) } end context 'with auth' do let(:auth_conn) { local_connection_with_auth('test', 'pass') } before do conn.user_add('root', 'pass') conn.user_grant_role('root', 'root') conn.user_add('test', 'pass') conn.auth_enable end after do conn.authenticate('root', 'pass') conn.auth_disable conn.user_delete('root') conn.user_delete('test') end it 'doesnt raise error' do expect{ auth_conn }.to_not raise_error end end context 'with a timeout' do it "sets the timeout in the kv handler" do etcd = local_connection_with_timeout(1.5) kv_handler = etcd.conn.connection.instance_variable_get("@handlers")[:kv] expect(kv_handler.instance_variable_get "@timeout").to eq(1.5) end it "sets a default timeout" do etcd = local_connection kv_handler = etcd.conn.connection.instance_variable_get("@handlers")[:kv] expect(kv_handler.instance_variable_get "@timeout").to eq(120) end end end describe '#version' do subject { conn.version } it { is_expected.to be_an_instance_of(String) } end describe '#db_size' do subject { conn.db_size } it { is_expected.to_not be_nil } end describe '#leader_id' do subject { conn.leader_id } it { is_expected.to_not be_nil } end describe '#alarm_list' do subject { conn.alarm_list } it { is_expected.to_not be_nil } end describe '#alarm_deactivate' do subject { conn.alarm_deactivate } it { is_expected.to_not be_nil } end describe '#get' do before do conn.put('apple', 'test') conn.put('applee', 'test') conn.put('appleee', 'test') end context 'no filters' do subject { conn.get('apple') } it { is_expected.to_not be_nil } end context 'sorts desc' do subject do conn.get('apple', range_end: 'appleeee', sort_order: :descend) \ .kvs.first.key end it { is_expected.to eq('appleee') } end context 'sorts asc' do subject do conn.get('apple', range_end: 'appleeee', sort_order: :ascend) \ .kvs.first.key end it { is_expected.to eq('apple') } end context 'count only' do subject do conn.get('apple', range_end: 'appleeee', count_only: true).kvs end it { is_expected.to be_empty } end it_should_behave_like "Etcdv3 instance using a timeout", :get, 'apple' end # Locking is not implemented in etcd v3.1.X unless $instance.version < Gem::Version.new("3.2.0") describe '#lock' do let(:lease_id) { lease_stub.lease_grant(10)['ID'] } subject { conn.lock('bar', lease_id) } it { is_expected.to be_an_instance_of(V3lockpb::LockResponse) } end describe '#with_lock' do let(:lease_id) { lease_stub.lease_grant(10)['ID'] } let(:lease_id_2) { lease_stub.lease_grant(15)['ID'] } it 'locks' do conn.with_lock('foobar', lease_id) do expect { conn.lock('foobar', lease_id_2, timeout: 0.1) } .to raise_error(GRPC::DeadlineExceeded) end end end end describe '#put' do subject { conn.put('test', 'value') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :put, 'test', 'value' end describe '#del' do context 'no range' do before { conn.put('test', 'value') } subject { conn.del('test') } it { is_expected.to_not be_nil } end context 'ranged del' do before do conn.put('test', 'value') conn.put('testt', 'value') end subject { conn.del('test', range_end: 'testtt') } it { is_expected.to_not be_nil } end it_should_behave_like "Etcdv3 instance using a timeout", :del, 'test' end describe '#lease_grant' do subject { conn.lease_grant(2) } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :lease_grant, 2 end describe '#lease_revoke' do let!(:lease_id) { conn.lease_grant(2)['ID'] } subject { conn.lease_revoke(lease_id) } it { is_expected.to_not be_nil } it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do conn.lease_revoke(lease_id, timeout: 0) end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect{ conn.lease_revoke(lease_id, timeout: 10) }.to_not raise_exception end end describe '#lease_ttl' do let!(:lease_id) { conn.lease_grant(2)['ID'] } subject { conn.lease_ttl(lease_id) } it { is_expected.to_not be_nil } it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do conn.lease_ttl(lease_id, timeout: 0) end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect{ conn.lease_ttl(lease_id, timeout: 10) }.to_not raise_exception end end describe '#watch' do let!(:foo) { conn.put('foo', 'bar') } subject { conn.watch('foo', start_revision: 1) } it { is_expected.to_not be_nil } it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do conn.watch('foo', timeout: 0) end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect{ conn.watch('foo', start_revision: 1, timeout: 10) }.to_not raise_exception end end describe '#lease_keep_alive_once' do let!(:lease_id) { conn.lease_grant(2)['ID'] } subject { conn.lease_keep_alive_once(lease_id) } it { is_expected.to_not be_nil } it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do conn.lease_keep_alive_once(lease_id, timeout: 0) end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect{ conn.lease_keep_alive_once(lease_id, timeout: 10) }.to_not raise_exception end end describe '#user_add' do after { conn.user_delete('test') rescue nil } subject { conn.user_add('test', 'user') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_add, 'test', 'user' end describe '#user_get' do after { conn.user_delete('test') rescue nil } before { conn.user_add('test', 'user') } subject { conn.user_get('test') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_get, 'test' end describe '#user_delete' do before { conn.user_add('test', 'user') rescue nil } subject { conn.user_delete('test') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_delete, 'test' end describe '#user_change_password' do before { conn.user_add('change_user', 'pass') } after { conn.user_delete('change_user') } subject { conn.user_change_password('change_user', 'new_pass') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_change_password, 'change_user', 'new_pass' end describe '#user_list' do subject { conn.user_list } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_list end describe '#role_list' do subject { conn.role_list } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :role_list end describe '#role_add' do subject { conn.role_add('role_add') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :role_add, 'role' end describe '#role_get' do before { conn.role_add('role_get') } after { conn.role_delete('role_get') } subject { conn.role_get('role_get') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :role_get, 'role_get' end describe '#role_delete' do before { conn.role_add('role_delete') } after { conn.role_delete('role_delete') rescue nil } subject { conn.role_delete('role_delete') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :role_delete, 'role_delete' end describe '#user_grant_role' do before { conn.user_add('grant_me', 'pass') } after { conn.user_delete('grant_me') rescue nil} subject { conn.user_grant_role('grant_me', 'root') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_grant_role, 'grant_me', 'root' end describe '#user_revoke_role' do before { conn.user_add('grant_me', 'pass') } before { conn.user_grant_role('grant_me', 'root') } after { conn.user_delete('grant_me') rescue nil} subject { conn.user_revoke_role('grant_me', 'root') } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :user_revoke_role, 'grant_me', 'root' end describe '#role_grant_permission' do before { conn.role_add('grant') } after { conn.role_delete('grant') } subject { conn.role_grant_permission('grant', :readwrite, 'a', **{range_end: 'Z'}) } it { is_expected.to_not be_nil } it_should_behave_like "Etcdv3 instance using a timeout", :role_grant_permission, 'grant', :readwrite, 'a' end describe '#role_revoke_permission' do before { conn.role_add('grant') } before { conn.role_grant_permission('grant', :readwrite, 'a', range_end: 'Z') } after { conn.role_delete('grant') } subject { conn.role_revoke_permission('grant', :readwrite, 'a', range_end: 'Z') } it { is_expected.to_not be_nil } describe "the timeouts" do before { conn.role_grant_permission('grant', :readwrite, 'a') } it_should_behave_like "Etcdv3 instance using a timeout", :role_revoke_permission, 'grant', :readwrite, 'a' end end describe '#auth_disable' do before do conn.user_add('root', 'test') conn.user_grant_role('root', 'root') conn.auth_enable conn.authenticate('root', 'test') end after { conn.user_delete('root') } after { conn.auth_disable } subject { conn.auth_disable } it { is_expected.to eq(true) } it_should_behave_like "Etcdv3 instance using a timeout", :auth_disable end describe '#auth_enable' do before do conn.user_add('root', 'test') conn.user_grant_role('root', 'root') end after do conn.authenticate('root', 'test') rescue nil conn.auth_disable conn.user_delete('root') end subject { conn.auth_enable } it { is_expected.to eq(true) } it_should_behave_like "Etcdv3 instance using a timeout", :auth_enable end describe "#authenticate" do context "auth enabled" do before do conn.user_add('root', 'test') conn.user_grant_role('root', 'root') conn.auth_enable conn.authenticate('root', 'test') end after do conn.auth_disable conn.user_delete('root') end it 'properly reconfigures auth and token' do expect(conn.token).to_not be_nil expect(conn.user).to eq('root') expect(conn.password).to eq('test') end end context 'auth disabled' do before do conn.user_add('root', 'root') conn.auth_disable end after do conn.user_delete('root') end it 'raises error' do expect { conn.authenticate('root', 'root') }.to raise_error(GRPC::FailedPrecondition) end end end describe '#transaction' do describe 'txn.value' do before { conn.put('txn', 'value') } after { conn.del('txn') } context 'success' do subject! do conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('success') end it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do conn.transaction(timeout: 0) do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect do conn.transaction(timeout: 1) do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end.to_not raise_exception end end context "success, value with lease" do let!(:lease_id) { conn.lease_grant(2)['ID'] } subject! do conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success', lease_id) ] txn.failure = [ txn.put('txn-test', 'failed', lease_id) ] end end it 'sets correct key, with a lease' do expect(conn.get('txn-test').kvs.first.value).to eq('success') expect(conn.get('txn-test').kvs.first.lease).to eq(lease_id) end end context 'failure' do subject! do conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'notright') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.create_revision' do before { conn.put('txn', 'value') } after { conn.del('txn') } context 'success' do subject! do conn.transaction do |txn| txn.compare = [ txn.create_revision('txn', :greater, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do conn.transaction do |txn| txn.compare = [ txn.create_revision('txn', :equal, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.mod_revision' do before { conn.put('txn', 'value') } after { conn.del('txn') } context 'success' do subject! do conn.transaction do |txn| txn.compare = [ txn.mod_revision('txn', :less, 1000) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do conn.transaction do |txn| txn.compare = [ txn.mod_revision('txn', :greater, 1000) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.version' do before { conn.put('txn', 'value') } after { conn.del('txn') } context 'success' do subject! do conn.transaction do |txn| txn.compare = [ txn.version('txn', :equal, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do conn.transaction do |txn| txn.compare = [ txn.version('txn', :equal, 100)] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(conn.get('txn-test').kvs.first.value).to eq('failed') end end end end describe "namespace" do describe '#get' do let(:get_conn) { local_connection_with_namespace("/namespace-get/") } before do conn.put('/apples/', 'app') conn.put('/namespace-get/apple', 'apple') conn.put('/namespace-get/apples', 'apples') conn.put('/namespace-get/appless', 'appless') end it 'returns key w/o namespace' do expect(get_conn.get("apple").kvs.last.value).to eq('apple') end it 'returns keys w/o namespace' do expect(get_conn.get("apple", range_end: 'applf').kvs.size).to eq(3) end it 'returns all keys under namespace' do expect(get_conn.get("", range_end: "\0").kvs.size).to eq(3) end end describe '#put' do let(:put_conn) { local_connection_with_namespace("/namespace-put/") } before do put_conn.put('apple_put', 'test') end it 'returns key with namespace' do expect(conn.get("/namespace-put/apple_put").kvs.last.value).to eq('test') end end describe '#del' do let(:del_conn) { local_connection_with_namespace("/del-test/") } context 'zero-byte' do before do del_conn.put('test', "key") del_conn.put('test2', "key2") conn.put('wall', 'zzzz') conn.put('walzz', 'adsfas') end it 'deleting all keys should be scoped to namespace' do resp = del_conn.del("", range_end: "\0") expect(resp.deleted).to eq(2) expect(conn.get("wall").kvs.last.value).to eq('zzzz') end end context 'no range' do before { del_conn.put('test', 'value') } subject { del_conn.del('test') } it { is_expected.to_not be_nil } end context 'ranged del' do before do del_conn.put('test', 'value') del_conn.put('testt', 'value') end subject { del_conn.del('test', range_end: 'testtt') } it { is_expected.to_not be_nil } end end describe '#transaction' do let(:trans_conn) { local_connection_with_namespace("/namespace/") } describe 'txn.value' do before { trans_conn.put('txn', 'value') } after { trans_conn.del('txn') } context 'success' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('success') expect(conn.get("/namespace/txn-test").kvs.first.value).to eq('success') end it "raises a GRPC::DeadlineExceeded exception when it takes too long" do expect do trans_conn.transaction(timeout: 0) do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end.to raise_exception(GRPC::DeadlineExceeded) end it "accepts a timeout" do expect do trans_conn.transaction(timeout: 1) do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end.to_not raise_exception end end context "success, value with lease" do let!(:lease_id) { trans_conn.lease_grant(2)['ID'] } subject! do trans_conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'value') ] txn.success = [ txn.put('txn-test', 'success', lease_id) ] txn.failure = [ txn.put('txn-test', 'failed', lease_id) ] end end it 'sets correct key, with a lease' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('success') expect(trans_conn.get('txn-test').kvs.first.lease).to eq(lease_id) end end context 'failure' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.value('txn', :equal, 'notright') ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.create_revision' do before { trans_conn.put('txn', 'value') } after { trans_conn.del('txn') } context 'success' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.create_revision('txn', :greater, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.create_revision('txn', :equal, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed') expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.mod_revision' do before { trans_conn.put('txn', 'value') } after { trans_conn.del('txn') } context 'success' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.mod_revision('txn', :less, 1000) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('success') expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.mod_revision('txn', :greater, 1000) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed') expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed') end end end describe 'txn.version' do before { trans_conn.put('txn-version', 'value') } after { trans_conn.del('txn-version') } context 'success' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.version('txn-version', :equal, 1) ] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('success') expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success') end end context 'failure' do subject! do trans_conn.transaction do |txn| txn.compare = [ txn.version('txn', :equal, 100)] txn.success = [ txn.put('txn-test', 'success') ] txn.failure = [ txn.put('txn-test', 'failed') ] end end it 'sets correct key' do expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed') expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed') end end end end # Locking is not implemented in etcd v3.1.X unless $instance.version < Gem::Version.new("3.2.0") describe "locking" do let(:ns_conn) { local_connection_with_namespace("/namespace/") } describe '#lock' do let(:lease_id) { lease_stub.lease_grant(10)['ID'] } subject { ns_conn.lock('mylocklock', lease_id) } it 'should lock key under specified namespace' do expect(conn.get("/namespace/#{subject.key}").kvs).to_not be_empty end end describe '#with_lock' do let(:lease_id) { lease_stub.lease_grant(10)['ID'] } let(:lease_id_2) { lease_stub.lease_grant(15)['ID'] } it 'enforces lock' do ns_conn.with_lock('mylock', lease_id) do expect { ns_conn.lock('mylock', lease_id_2, timeout: 0.1) } .to raise_error(GRPC::DeadlineExceeded) end end end end end end end end