diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..835dcd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.ruby-version +Gemfile.lock +vendor diff --git a/.travis.yml b/.travis.yml index e988f97..129267b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ cache: bundler sudo: false language: ruby +services: + - redis-server rvm: - 1.9.3 - 2.1.1 diff --git a/spec/helpers.rb b/spec/helpers.rb index a91c987..d28b761 100644 --- a/spec/helpers.rb +++ b/spec/helpers.rb @@ -1,7 +1,12 @@ -def expect_json( - ok = true, - http = 200 -) +def redis + unless @redis + @redis = Redis.new + @redis.select(15) # let's use the highest numbered database available in a default install + end + @redis +end + +def expect_json(ok = true, http = 200) expect(last_response.header['Content-Type']).to eq('application/json') if (ok == true) then @@ -12,3 +17,59 @@ def expect_json( expect(last_response.status).to eq(http) end + +def create_token(token, user, timestamp) + redis.hset("vmpooler__token__#{token}", 'user', user) + redis.hset("vmpooler__token__#{token}", 'created', timestamp) +end + +def get_token_data(token) + redis.hgetall("vmpooler__token__#{token}") +end + +def token_exists?(token) + result = get_token_data + result && !result.empty? +end + +def create_ready_vm(template, name, token = nil) + create_vm(name, token) + redis.sadd("vmpooler__ready__#{template}", name) + redis.hset("vmpooler_vm_#{name}", "template", template) +end + +def create_running_vm(template, name, token = nil) + create_vm(name, token) + redis.sadd("vmpooler__running__#{template}", name) + redis.hset("vmpooler__vm__#{name}", "template", template) +end + +def create_vm(name, token = nil) + redis.hset("vmpooler__vm__#{name}", 'checkout', Time.now) + if token + redis.hset("vmpooler__vm__#{name}", 'token:token', token) + end +end + +def fetch_vm(vm) + redis.hgetall("vmpooler__vm__#{vm}") +end + +def snapshot_vm(vm, snapshot = '12345678901234567890123456789012') + redis.sadd('vmpooler__tasks__snapshot', "#{vm}:#{snapshot}") + redis.hset("vmpooler__vm__#{vm}", "snapshot:#{snapshot}", "1") +end + +def has_vm_snapshot?(vm) + redis.smembers('vmpooler__tasks__snapshot').any? do |snapshot| + instance, sha = snapshot.split(':') + vm == instance + end +end + +def vm_reverted_to_snapshot?(vm, snapshot = nil) + redis.smembers('vmpooler__tasks__snapshot-revert').any? do |action| + instance, sha = action.split(':') + instance == vm and (snapshot ? (sha == snapshot) : true) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2186bdb..7589276 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,3 +2,4 @@ require 'helpers' require 'rbvmomi' require 'rspec' require 'vmpooler' +require 'redis' diff --git a/spec/vmpooler/api/v1/token_spec.rb b/spec/vmpooler/api/v1/token_spec.rb new file mode 100644 index 0000000..d386457 --- /dev/null +++ b/spec/vmpooler/api/v1/token_spec.rb @@ -0,0 +1,173 @@ +require 'spec_helper' +require 'rack/test' + +module Vmpooler + class API + module Helpers + def authenticate(auth, username_str, password_str) + username_str == 'admin' and password_str == 's3cr3t' + end + end + end +end + +describe Vmpooler::API::V1 do + include Rack::Test::Methods + + def app() + Vmpooler::API + end + + describe '/token' do + let(:prefix) { '/api/v1' } + let(:current_time) { Time.now } + let(:config) { { } } + + before do + app.settings.set :config, config + app.settings.set :redis, redis + end + + describe 'GET /token' do + context '(auth not configured)' do + let(:config) { { auth: false } } + + it 'returns a 404' do + get "#{prefix}/token" + expect_json(ok = false, http = 404) + end + end + + context '(auth configured)' do + let(:config) { { auth: true } } + + it 'returns a 401 if not authed' do + get "#{prefix}/token" + expect_json(ok = false, http = 401) + end + + it 'returns a list of tokens if authed' do + create_token "abc", "admin", current_time + + authorize 'admin', 's3cr3t' + get "#{prefix}/token" + expect_json(ok = true, http = 200) + + expect(JSON.parse(last_response.body)['abc']['created']).to eq(current_time.to_s) + end + end + end + + describe 'POST /token' do + context '(auth not configured)' do + let(:config) { { auth: false } } + + it 'returns a 404' do + post "#{prefix}/token" + expect_json(ok = false, http = 404) + end + end + + context '(auth configured)' do + let(:config) { { auth: true } } + + it 'returns a 401 if not authed' do + post "#{prefix}/token" + expect_json(ok = false, http = 401) + end + + it 'returns a newly created token if authed' do + authorize 'admin', 's3cr3t' + post "#{prefix}/token" + expect_json(ok = true, http = 200) + + returned_token = JSON.parse(last_response.body)['token'] + expect(returned_token.length).to be(32) + expect(get_token_data(returned_token)['user']).to eq("admin") + end + end + end + end + + describe '/token/:token' do + let(:prefix) { '/api/v1' } + let(:current_time) { Time.now } + + before do + app.settings.set :config, config + app.settings.set :redis, redis + end + + def create_vm_for_token(token, pool, vm) + redis.sadd("vmpooler__running__#{pool}", vm) + redis.hset("vmpooler__vm__#{vm}", "token:token", token) + end + + describe 'GET /token/:token' do + context '(auth not configured)' do + let(:config) { { auth: false } } + + it 'returns a 404' do + get "#{prefix}/token/this" + expect_json(ok = false, http = 404) + end + end + + context '(auth configured)' do + let(:config) { { + auth: true, + pools: [ + {'name' => 'pool1', 'size' => 5} + ] + } } + + it 'returns a token' do + create_token "mytoken", "admin", current_time + create_vm_for_token "mytoken", "pool1", "vmhostname" + + get "#{prefix}/token/mytoken" + expect_json(ok = true, http = 200) + + expect(JSON.parse(last_response.body)['ok']).to eq(true) + expect(JSON.parse(last_response.body)['mytoken']['user']).to eq('admin') + expect(JSON.parse(last_response.body)['mytoken']['vms']['running']).to include('vmhostname') + end + end + end + + describe 'DELETE /token/:token' do + context '(auth not configured)' do + let(:config) { { auth: false } } + + it 'returns a 404' do + delete "#{prefix}/token/this" + expect_json(ok = false, http = 404) + end + end + + context '(auth configured)' do + let(:config) { { auth: true } } + + it 'returns a 401 if not authed' do + delete "#{prefix}/token/this" + expect_json(ok = false, http = 401) + end + + it 'deletes a token if authed' do + create_token("mytoken", "admin", current_time) + authorize 'admin', 's3cr3t' + + delete "#{prefix}/token/mytoken" + expect_json(ok = true, http = 200) + end + + it 'fails if token does not exist' do + authorize 'admin', 's3cr3t' + + delete "#{prefix}/token/missingtoken" + expect_json(ok = false, http = 401) # TODO: should this be 404? + end + end + end + end +end diff --git a/spec/vmpooler/api/v1/vm_hostname_spec.rb b/spec/vmpooler/api/v1/vm_hostname_spec.rb new file mode 100644 index 0000000..f5dce5b --- /dev/null +++ b/spec/vmpooler/api/v1/vm_hostname_spec.rb @@ -0,0 +1,317 @@ +require 'spec_helper' +require 'rack/test' + +module Vmpooler + class API + module Helpers + def authenticate(auth, username_str, password_str) + username_str == 'admin' and password_str == 's3cr3t' + end + end + end +end + +def has_set_tag?(vm, tag, value) + value == redis.hget("vmpooler__vm__#{vm}", "tag:#{tag}") +end + +describe Vmpooler::API::V1 do + include Rack::Test::Methods + + def app() + Vmpooler::API + end + + describe '/vm/:hostname' do + let(:prefix) { '/api/v1' } + + let(:config) { + { + config: { + 'site_name' => 'test pooler', + 'vm_lifetime_auth' => 2, + }, + pools: [ + {'name' => 'pool1', 'size' => 5}, + {'name' => 'pool2', 'size' => 10} + ], + alias: { 'poolone' => 'pool1' }, + } + } + + let(:current_time) { Time.now } + + before(:each) do + redis.flushdb + + app.settings.set :config, config + app.settings.set :redis, redis + app.settings.set :config, auth: false + create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) + end + + describe 'PUT /vm/:hostname' do + it 'allows tags to be set' do + create_vm('testhost') + put "#{prefix}/vm/testhost", '{"tags":{"tested_by":"rspec"}}' + expect_json(ok = true, http = 200) + + expect has_set_tag?('testhost', 'tested_by', 'rspec') + end + + it 'skips empty tags' do + create_vm('testhost') + put "#{prefix}/vm/testhost", '{"tags":{"tested_by":""}}' + expect_json(ok = true, http = 200) + + expect !has_set_tag?('testhost', 'tested_by', '') + end + + it 'does not set tags if request body format is invalid' do + create_vm('testhost') + put "#{prefix}/vm/testhost", '{"tags":{"tested"}}' + expect_json(ok = false, http = 400) + + expect !has_set_tag?('testhost', 'tested', '') + end + + context '(allowed_tags configured)' do + it 'fails if specified tag is not in allowed_tags array' do + app.settings.set :config, + { :config => { 'allowed_tags' => ['created_by', 'project', 'url'] } } + + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"tags":{"created_by":"rspec","tested_by":"rspec"}}' + expect_json(ok = false, http = 400) + + expect !has_set_tag?('testhost', 'tested_by', 'rspec') + end + end + + context '(tagfilter configured)' do + let(:config) { { + tagfilter: { 'url' => '(.*)\/' }, + } } + + it 'correctly filters tags' do + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com/something.html"}}' + expect_json(ok = true, http = 200) + + expect has_set_tag?('testhost', 'url', 'foo.com') + end + + it "doesn't eat tags not matching filter" do + create_vm('testhost') + put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com"}}' + expect_json(ok = true, http = 200) + + expect has_set_tag?('testhost', 'url', 'foo.com') + end + end + + context '(auth not configured)' do + let(:config) { { auth: false } } + + it 'allows VM lifetime to be modified without a token' do + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"lifetime":"1"}' + expect_json(ok = true, http = 200) + + vm = fetch_vm('testhost') + expect(vm['lifetime'].to_i).to eq(1) + end + + it 'does not allow a lifetime to be 0' do + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"lifetime":"0"}' + expect_json(ok = false, http = 400) + + vm = fetch_vm('testhost') + expect(vm['lifetime']).to be_nil + end + end + + context '(auth configured)' do + before(:each) do + app.settings.set :config, auth: true + end + + it 'allows VM lifetime to be modified with a token' do + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"lifetime":"1"}', { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + vm = fetch_vm('testhost') + expect(vm['lifetime'].to_i).to eq(1) + end + + it 'does not allows VM lifetime to be modified without a token' do + create_vm('testhost') + + put "#{prefix}/vm/testhost", '{"lifetime":"1"}' + expect_json(ok = false, http = 401) + end + end + end + + describe 'DELETE /vm/:hostname' do + context '(auth not configured)' do + it 'does not delete a non-existant VM' do + delete "#{prefix}/vm/testhost" + expect_json(ok = false, http = 404) + end + + it 'deletes an existing VM' do + create_running_vm('pool1', 'testhost') + expect fetch_vm('testhost') + + delete "#{prefix}/vm/testhost" + expect_json(ok = true, http = 200) + expect !fetch_vm('testhost') + end + end + + context '(auth configured)' do + before(:each) do + app.settings.set :config, auth: true + end + + context '(checked-out without token)' do + it 'deletes a VM without supplying a token' do + create_running_vm('pool1', 'testhost') + expect fetch_vm('testhost') + + delete "#{prefix}/vm/testhost" + expect_json(ok = true, http = 200) + expect !fetch_vm('testhost') + end + end + + context '(checked-out with token)' do + it 'fails to delete a VM without supplying a token' do + create_running_vm('pool1', 'testhost', 'abcdefghijklmnopqrstuvwxyz012345') + expect fetch_vm('testhost') + + delete "#{prefix}/vm/testhost" + expect_json(ok = false, http = 401) + expect fetch_vm('testhost') + end + + it 'deletes a VM when token is supplied' do + create_running_vm('pool1', 'testhost', 'abcdefghijklmnopqrstuvwxyz012345') + expect fetch_vm('testhost') + + delete "#{prefix}/vm/testhost", "", { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + expect !fetch_vm('testhost') + end + end + end + end + + describe 'POST /vm/:hostname/snapshot' do + context '(auth not configured)' do + it 'creates a snapshot' do + create_vm('testhost') + post "#{prefix}/vm/testhost/snapshot" + expect_json(ok = true, http = 202) + expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32) + end + end + + context '(auth configured)' do + before(:each) do + app.settings.set :config, auth: true + end + + it 'returns a 401 if not authed' do + post "#{prefix}/vm/testhost/snapshot" + expect_json(ok = false, http = 401) + expect !has_vm_snapshot?('testhost') + end + + it 'creates a snapshot if authed' do + create_vm('testhost') + snapshot_vm('testhost', 'testsnapshot') + + post "#{prefix}/vm/testhost/snapshot", "", { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 202) + expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32) + expect has_vm_snapshot?('testhost') + end + end + end + + describe 'POST /vm/:hostname/snapshot/:snapshot' do + context '(auth not configured)' do + it 'reverts to a snapshot' do + create_vm('testhost') + snapshot_vm('testhost', 'testsnapshot') + + post "#{prefix}/vm/testhost/snapshot/testsnapshot" + expect_json(ok = true, http = 202) + expect vm_reverted_to_snapshot?('testhost', 'testsnapshot') + end + + it 'fails if the specified snapshot does not exist' do + create_vm('testhost') + + post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = false, http = 404) + expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot') + end + end + + context '(auth configured)' do + before(:each) do + app.settings.set :config, auth: true + end + + it 'returns a 401 if not authed' do + create_vm('testhost') + snapshot_vm('testhost', 'testsnapshot') + + post "#{prefix}/vm/testhost/snapshot/testsnapshot" + expect_json(ok = false, http = 401) + expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot') + end + + it 'fails if authed and the specified snapshot does not exist' do + create_vm('testhost') + + post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = false, http = 404) + expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot') + end + + it 'reverts to a snapshot if authed' do + create_vm('testhost') + snapshot_vm('testhost', 'testsnapshot') + + post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 202) + expect vm_reverted_to_snapshot?('testhost', 'testsnapshot') + end + end + end + end +end diff --git a/spec/vmpooler/api/v1/vm_spec.rb b/spec/vmpooler/api/v1/vm_spec.rb new file mode 100644 index 0000000..6a4a068 --- /dev/null +++ b/spec/vmpooler/api/v1/vm_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' +require 'rack/test' + +module Vmpooler + class API + module Helpers + def authenticate(auth, username_str, password_str) + username_str == 'admin' and password_str == 's3cr3t' + end + end + end +end + +describe Vmpooler::API::V1 do + include Rack::Test::Methods + + def app() + Vmpooler::API + end + + describe '/vm' do + let(:prefix) { '/api/v1' } + + let(:config) { + { + config: { + 'site_name' => 'test pooler', + 'vm_lifetime_auth' => 2, + }, + pools: [ + {'name' => 'pool1', 'size' => 5}, + {'name' => 'pool2', 'size' => 10} + ], + alias: { 'poolone' => 'pool1' }, + } + } + + let(:current_time) { Time.now } + + before(:each) do + redis.flushdb + + app.settings.set :config, config + app.settings.set :redis, redis + app.settings.set :config, auth: false + create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) + end + + describe 'POST /vm' do + it 'returns a single VM' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm", '{"pool1":"1"}' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'returns a single VM for an alias' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm", '{"poolone":"1"}' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'fails on nonexistant pools' do + post "#{prefix}/vm", '{"poolpoolpool":"1"}' + expect_json(ok = false, http = 404) + end + + it 'returns multiple VMs' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + create_ready_vm 'pool2', 'qrstuvwxyz012345' + + post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + }, + pool2: { + hostname: 'qrstuvwxyz012345' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + context '(auth not configured)' do + it 'does not extend VM lifetime if auth token is provided' do + app.settings.set :config, auth: false + + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm", '{"pool1":"1"}', { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime']).to be_nil + end + end + + context '(auth configured)' do + it 'extends VM lifetime if auth token is provided' do + app.settings.set :config, auth: true + + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm", '{"pool1":"1"}', { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime'].to_i).to eq(2) + end + + it 'does not extend VM lifetime if auth token is not provided' do + app.settings.set :config, auth: true + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm", '{"pool1":"1"}' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime']).to be_nil + end + end + end + end +end diff --git a/spec/vmpooler/api/v1/vm_template_spec.rb b/spec/vmpooler/api/v1/vm_template_spec.rb new file mode 100644 index 0000000..28b85c3 --- /dev/null +++ b/spec/vmpooler/api/v1/vm_template_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' +require 'rack/test' + +module Vmpooler + class API + module Helpers + def authenticate(auth, username_str, password_str) + username_str == 'admin' and password_str == 's3cr3t' + end + end + end +end + +describe Vmpooler::API::V1 do + include Rack::Test::Methods + + def app() + Vmpooler::API + end + + describe '/vm/:template' do + let(:prefix) { '/api/v1' } + + let(:config) { + { + config: { + 'site_name' => 'test pooler', + 'vm_lifetime_auth' => 2, + }, + pools: [ + {'name' => 'pool1', 'size' => 5}, + {'name' => 'pool2', 'size' => 10} + ], + alias: { 'poolone' => 'pool1' }, + } + } + + let(:current_time) { Time.now } + + before(:each) do + redis.flushdb + + app.settings.set :config, config + app.settings.set :redis, redis + app.settings.set :config, auth: false + create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) + end + + describe 'POST /vm/:template' do + it 'returns a single VM' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm/pool1", '' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'returns a single VM for an alias' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm/poolone", '' + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect_json(ok = true, http = 200) + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'fails on nonexistant pools' do + post "#{prefix}/vm/poolpoolpool", '' + expect_json(ok = false, http = 404) + end + + it 'returns multiple VMs' do + create_ready_vm 'pool1', 'abcdefghijklmnop' + create_ready_vm 'pool2', 'qrstuvwxyz012345' + + post "#{prefix}/vm/pool1+pool2", '' + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + }, + pool2: { + hostname: 'qrstuvwxyz012345' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + context '(auth not configured)' do + it 'does not extend VM lifetime if auth token is provided' do + app.settings.set :config, auth: false + + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm/pool1", '', { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime']).to be_nil + end + end + + context '(auth configured)' do + it 'extends VM lifetime if auth token is provided' do + app.settings.set :config, auth: true + + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm/pool1", '', { + 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' + } + expect_json(ok = true, http = 200) + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime'].to_i).to eq(2) + end + + it 'does not extend VM lifetime if auth token is not provided' do + app.settings.set :config, auth: true + create_ready_vm 'pool1', 'abcdefghijklmnop' + + post "#{prefix}/vm/pool1", '' + + expected = { + ok: true, + pool1: { + hostname: 'abcdefghijklmnop' + } + } + expect_json(ok = true, http = 200) + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + + vm = fetch_vm('abcdefghijklmnop') + expect(vm['lifetime']).to be_nil + end + end + end + end +end diff --git a/spec/vmpooler/api/v1_spec.rb b/spec/vmpooler/api/v1_spec.rb deleted file mode 100644 index 1b65962..0000000 --- a/spec/vmpooler/api/v1_spec.rb +++ /dev/null @@ -1,735 +0,0 @@ -require 'spec_helper' -require 'rack/test' - -module Vmpooler - class API - module Helpers - def authenticate(auth, username_str, password_str) - username_str == 'admin' and password_str == 's3cr3t' - end - end - end -end - -describe Vmpooler::API::V1 do - include Rack::Test::Methods - - def app() - Vmpooler::API - end - - describe '/token' do - let(:redis) { double('redis') } - let(:prefix) { '/api/v1' } - - before do - app.settings.set :config, config - app.settings.set :redis, redis - end - - describe 'GET /token' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'returns a 404' do - get "#{prefix}/token" - - expect_json(ok = false, http = 404) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'returns a 401 if not authed' do - get "#{prefix}/token" - - expect_json(ok = false, http = 401) - end - - it 'returns a list of tokens if authed' do - expect(redis).to receive(:keys).with('vmpooler__token__*').and_return(["vmpooler__token__abc"]) - expect(redis).to receive(:hgetall).with('vmpooler__token__abc').and_return({"user" => "admin", "created" => "now"}) - - authorize 'admin', 's3cr3t' - - get "#{prefix}/token" - - expect(JSON.parse(last_response.body)['abc']['created']).to eq('now') - - expect_json(ok = true, http = 200) - end - end - end - - describe 'POST /token' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'returns a 404' do - post "#{prefix}/token" - - expect_json(ok = false, http = 404) - end - end - - context '(auth configured)' do - before do - allow(redis).to receive(:hset).and_return '1' - end - - let(:config) { { auth: true } } - - it 'returns a 401 if not authed' do - post "#{prefix}/token" - - expect_json(ok = false, http = 401) - end - - it 'returns a token if authed' do - authorize 'admin', 's3cr3t' - - post "#{prefix}/token" - - expect(JSON.parse(last_response.body)['token'].length).to be(32) - - expect_json(ok = true, http = 200) - end - end - end - end - - describe '/token/:token' do - let(:redis) { double('redis') } - let(:prefix) { '/api/v1' } - - before do - app.settings.set :config, config - app.settings.set :redis, redis - end - - describe 'GET /token/:token' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'returns a 404' do - get "#{prefix}/token/this" - - expect_json(ok = false, http = 404) - end - end - - context '(auth configured)' do - let(:config) { { - auth: true, - pools: [ - {'name' => 'pool1', 'size' => 5} - ] - } } - - it 'returns a token' do - expect(redis).to receive(:hgetall).with('vmpooler__token__this').and_return({'user' => 'admin'}) - expect(redis).to receive(:smembers).with('vmpooler__running__pool1').and_return(['vmhostname']) - expect(redis).to receive(:hget).with('vmpooler__vm__vmhostname', 'token:token').and_return('this') - - get "#{prefix}/token/this" - - expect(JSON.parse(last_response.body)['ok']).to eq(true) - expect(JSON.parse(last_response.body)['this']['user']).to eq('admin') - expect(JSON.parse(last_response.body)['this']['vms']['running']).to include('vmhostname') - - expect_json(ok = true, http = 200) - end - end - end - - describe 'DELETE /token/:token' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'returns a 404' do - delete "#{prefix}/token/this" - - expect_json(ok = false, http = 404) - end - end - - context '(auth configured)' do - before do - allow(redis).to receive(:del).and_return '1' - end - - let(:config) { { auth: true } } - - it 'returns a 401 if not authed' do - delete "#{prefix}/token/this" - - expect_json(ok = false, http = 401) - end - - it 'deletes a token if authed' do - authorize 'admin', 's3cr3t' - - delete "#{prefix}/token/this" - - expect_json(ok = true, http = 200) - end - end - end - end - - describe '/vm' do - let(:redis) { double('redis') } - let(:prefix) { '/api/v1' } - let(:config) { { - config: { - 'site_name' => 'test pooler', - 'vm_lifetime_auth' => 2 - }, - pools: [ - {'name' => 'pool1', 'size' => 5}, - {'name' => 'pool2', 'size' => 10} - ], - alias: { 'poolone' => 'pool1' } - } } - - before do - app.settings.set :config, config - app.settings.set :redis, redis - - allow(redis).to receive(:exists).and_return '1' - allow(redis).to receive(:hget).with('vmpooler__token__abcdefghijklmnopqrstuvwxyz012345', 'user').and_return 'jdoe' - allow(redis).to receive(:hset).and_return '1' - allow(redis).to receive(:sadd).and_return '1' - allow(redis).to receive(:scard).and_return '5' - allow(redis).to receive(:spop).with('vmpooler__ready__pool1').and_return 'abcdefghijklmnop' - allow(redis).to receive(:spop).with('vmpooler__ready__pool2').and_return 'qrstuvwxyz012345' - end - - describe 'POST /vm' do - it 'returns a single VM' do - post "#{prefix}/vm", '{"pool1":"1"}' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'returns a single VM for an alias' do - expect(redis).to receive(:exists).with("vmpooler__ready__poolone").and_return(false) - - post "#{prefix}/vm", '{"poolone":"1"}' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'fails on nonexistant pools' do - expect(redis).to receive(:exists).with("vmpooler__ready__poolpoolpool").and_return(false) - - post "#{prefix}/vm", '{"poolpoolpool":"1"}' - - expect_json(ok = false, http = 404) - end - - it 'returns multiple VMs' do - post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - }, - pool2: { - hostname: 'qrstuvwxyz012345' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'does not extend VM lifetime if auth token is provided' do - expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2) - - post "#{prefix}/vm", '{"pool1":"1"}', { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'extends VM lifetime if auth token is provided' do - expect(redis).to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2).once - - post "#{prefix}/vm", '{"pool1":"1"}', { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'does not extend VM lifetime if auth token is not provided' do - expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2) - - post "#{prefix}/vm", '{"pool1":"1"}' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - end - end - end - - describe '/vm/:template' do - let(:redis) { double('redis') } - let(:prefix) { '/api/v1' } - let(:config) { { - config: { - 'site_name' => 'test pooler', - 'vm_lifetime_auth' => 2 - }, - pools: [ - {'name' => 'pool1', 'size' => 5}, - {'name' => 'pool2', 'size' => 10} - ], - alias: { 'poolone' => 'pool1' } - } } - - before do - app.settings.set :config, config - app.settings.set :redis, redis - - allow(redis).to receive(:exists).and_return '1' - allow(redis).to receive(:hget).with('vmpooler__token__abcdefghijklmnopqrstuvwxyz012345', 'user').and_return 'jdoe' - allow(redis).to receive(:hset).and_return '1' - allow(redis).to receive(:sadd).and_return '1' - allow(redis).to receive(:scard).and_return '5' - allow(redis).to receive(:spop).with('vmpooler__ready__pool1').and_return 'abcdefghijklmnop' - allow(redis).to receive(:spop).with('vmpooler__ready__pool2').and_return 'qrstuvwxyz012345' - end - - describe 'POST /vm/:template' do - it 'returns a single VM' do - post "#{prefix}/vm/pool1", '' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'returns a single VM for an alias' do - expect(redis).to receive(:exists).with("vmpooler__ready__poolone").and_return(false) - - post "#{prefix}/vm/poolone", '' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'fails on nonexistant pools' do - expect(redis).to receive(:exists).with("vmpooler__ready__poolpoolpool").and_return(false) - - post "#{prefix}/vm/poolpoolpool", '' - - expect_json(ok = false, http = 404) - end - - it 'returns multiple VMs' do - post "#{prefix}/vm/pool1+pool2", '' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - }, - pool2: { - hostname: 'qrstuvwxyz012345' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'does not extend VM lifetime if auth token is provided' do - expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2) - - post "#{prefix}/vm/pool1", '', { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'extends VM lifetime if auth token is provided' do - expect(redis).to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2).once - - post "#{prefix}/vm/pool1", '', { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - - it 'does not extend VM lifetime if auth token is not provided' do - expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2) - - post "#{prefix}/vm/pool1", '' - - expected = { - ok: true, - pool1: { - hostname: 'abcdefghijklmnop' - } - } - - expect(last_response.body).to eq(JSON.pretty_generate(expected)) - - expect_json(ok = true, http = 200) - end - end - end - end - - describe '/vm/:hostname' do - let(:redis) { double('redis') } - let(:prefix) { '/api/v1' } - let(:config) { { - pools: [ - {'name' => 'pool1', 'size' => 5}, - {'name' => 'pool2', 'size' => 10} - ] - } } - - before do - app.settings.set :config, config - app.settings.set :redis, redis - - allow(redis).to receive(:exists).and_return '1' - allow(redis).to receive(:hset).and_return '1' - end - - describe 'PUT /vm/:hostname' do - it 'allows tags to be set' do - put "#{prefix}/vm/testhost", '{"tags":{"tested_by":"rspec"}}' - - expect_json(ok = true, http = 200) - end - - it 'skips empty tags' do - put "#{prefix}/vm/testhost", '{"tags":{"tested_by":""}}' - - expect_json(ok = true, http = 200) - end - - it 'does not set tags if request body format is invalid' do - put "#{prefix}/vm/testhost", '{"tags":{"tested"}}' - - expect_json(ok = false, http = 400) - end - - context '(allowed_tags configured)' do - let(:config) { { - config: { - 'allowed_tags' => ['created_by', 'project', 'url'] - } - } } - - it 'fails if specified tag is not in allowed_tags array' do - put "#{prefix}/vm/testhost", '{"tags":{"created_by":"rspec","tested_by":"rspec"}}' - - expect_json(ok = false, http = 400) - end - end - - context '(tagfilter configured)' do - let(:config) { { - tagfilter: { 'url' => '(.*)\/' }, - } } - - it 'correctly filters tags' do - expect(redis).to receive(:hset).with("vmpooler__vm__testhost", "tag:url", "foo.com") - - put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com/something.html"}}' - - expect_json(ok = true, http = 200) - end - - it 'doesn\'t eat tags not matching filter' do - expect(redis).to receive(:hset).with("vmpooler__vm__testhost", "tag:url", "foo.com") - - put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com"}}' - - expect_json(ok = true, http = 200) - end - end - - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'allows VM lifetime to be modified without a token' do - put "#{prefix}/vm/testhost", '{"lifetime":"1"}' - - expect_json(ok = true, http = 200) - end - - it 'does not allow a lifetime to be 0' do - put "#{prefix}/vm/testhost", '{"lifetime":"0"}' - - expect_json(ok = false, http = 400) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'allows VM lifetime to be modified with a token' do - put "#{prefix}/vm/testhost", '{"lifetime":"1"}', { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expect_json(ok = true, http = 200) - end - - it 'does not allows VM lifetime to be modified without a token' do - put "#{prefix}/vm/testhost", '{"lifetime":"1"}' - - expect_json(ok = false, http = 401) - end - end - end - - describe 'DELETE /vm/:hostname' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'does not delete a non-existant VM' do - expect(redis).to receive(:hgetall).and_return({}) - expect(redis).not_to receive(:sadd) - expect(redis).not_to receive(:srem) - - delete "#{prefix}/vm/testhost" - - expect_json(ok = false, http = 404) - end - - it 'deletes an existing VM' do - expect(redis).to receive(:hgetall).with('vmpooler__vm__testhost').and_return({"template" => "pool1"}) - expect(redis).to receive(:srem).and_return(true) - expect(redis).to receive(:sadd) - - delete "#{prefix}/vm/testhost" - - expect_json(ok = true, http = 200) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - context '(checked-out without token)' do - it 'deletes a VM without supplying a token' do - expect(redis).to receive(:hgetall).with('vmpooler__vm__testhost').and_return({"template" => "pool1"}) - expect(redis).to receive(:srem).and_return(true) - expect(redis).to receive(:sadd) - - delete "#{prefix}/vm/testhost" - - expect_json(ok = true, http = 200) - end - end - - context '(checked-out with token)' do - it 'fails to delete a VM without supplying a token' do - expect(redis).to receive(:hgetall).with('vmpooler__vm__testhost').and_return({"template" => "pool1", "token:token" => "abcdefghijklmnopqrstuvwxyz012345"}) - expect(redis).not_to receive(:sadd) - expect(redis).not_to receive(:srem) - - delete "#{prefix}/vm/testhost" - - expect_json(ok = false, http = 401) - end - - it 'deletes a VM when token is supplied' do - expect(redis).to receive(:hgetall).with('vmpooler__vm__testhost').and_return({"template" => "pool1", "token:token" => "abcdefghijklmnopqrstuvwxyz012345"}) - expect(redis).to receive(:srem).and_return(true) - expect(redis).to receive(:sadd) - - delete "#{prefix}/vm/testhost", "", { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expect_json(ok = true, http = 200) - end - end - end - end - - describe 'POST /vm/:hostname/snapshot' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'creates a snapshot' do - expect(redis).to receive(:sadd) - - post "#{prefix}/vm/testhost/snapshot" - - expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32) - - expect_json(ok = true, http = 202) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'returns a 401 if not authed' do - post "#{prefix}/vm/testhost/snapshot" - - expect_json(ok = false, http = 401) - end - - it 'creates a snapshot if authed' do - expect(redis).to receive(:sadd) - - post "#{prefix}/vm/testhost/snapshot", "", { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32) - - expect_json(ok = true, http = 202) - end - end - end - - describe 'POST /vm/:hostname/snapshot/:snapshot' do - context '(auth not configured)' do - let(:config) { { auth: false } } - - it 'reverts to a snapshot' do - expect(redis).to receive(:hget).with('vmpooler__vm__testhost', 'snapshot:testsnapshot').and_return(1) - expect(redis).to receive(:sadd) - - post "#{prefix}/vm/testhost/snapshot/testsnapshot" - - expect_json(ok = true, http = 202) - end - end - - context '(auth configured)' do - let(:config) { { auth: true } } - - it 'returns a 401 if not authed' do - post "#{prefix}/vm/testhost/snapshot" - - expect_json(ok = false, http = 401) - end - - it 'reverts to a snapshot if authed' do - expect(redis).to receive(:hget).with('vmpooler__vm__testhost', 'snapshot:testsnapshot').and_return(1) - expect(redis).to receive(:sadd) - - post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", { - 'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345' - } - - expect_json(ok = true, http = 202) - end - end - - end - end - -end diff --git a/spec/vmpooler/api_spec.rb b/spec/vmpooler/dashboard_spec.rb similarity index 76% rename from spec/vmpooler/api_spec.rb rename to spec/vmpooler/dashboard_spec.rb index 246503f..06d2a86 100644 --- a/spec/vmpooler/api_spec.rb +++ b/spec/vmpooler/dashboard_spec.rb @@ -10,6 +10,10 @@ describe Vmpooler::API do describe 'Dashboard' do + before(:each) do + redis.flushdb + end + context '/' do before { get '/' } @@ -38,7 +42,6 @@ describe Vmpooler::API do it { expect(last_response.status).to eq(404) } it { expect(last_response.header['Content-Type']).to eq('application/json') } it { expect(last_response.body).to eq(JSON.pretty_generate({ok: false})) } - end describe '/dashboard/stats/vmpooler/pool' do @@ -49,7 +52,6 @@ describe Vmpooler::API do ], graphite: {} } } - let(:redis) { double('redis') } before do $config = config @@ -59,13 +61,12 @@ describe Vmpooler::API do end context 'without history param' do - it 'returns basic JSON' do - allow(redis).to receive(:scard) - allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(3) - allow(redis).to receive(:scard).with('vmpooler__ready__pool2').and_return(2) - - expect(redis).to receive(:scard).twice + create_ready_vm('pool1', 'vm1') + create_ready_vm('pool1', 'vm2') + create_ready_vm('pool1', 'vm3') + create_ready_vm('pool2', 'vm4') + create_ready_vm('pool2', 'vm5') get '/dashboard/stats/vmpooler/pool' @@ -78,19 +79,15 @@ describe Vmpooler::API do expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.header['Content-Type']).to eq('application/json') end - end context 'with history param' do - it 'returns JSON with null history when redis does not has values' do - allow(redis).to receive(:scard) - expect(redis).to receive(:scard).exactly(4).times - + it 'returns JSON with zeroed history when redis does not have values' do get '/dashboard/stats/vmpooler/pool', :history => true json_hash = { - pool1: {size: 5, ready: nil, history: [nil]}, - pool2: {size: 1, ready: nil, history: [nil]} + pool1: {size: 5, ready: 0, history: [0]}, + pool2: {size: 1, ready: 0, history: [0]} } expect(last_response).to be_ok @@ -99,10 +96,11 @@ describe Vmpooler::API do end it 'returns JSON with history when redis has values' do - allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(3) - allow(redis).to receive(:scard).with('vmpooler__ready__pool2').and_return(2) - - expect(redis).to receive(:scard).exactly(4).times + create_ready_vm('pool1', 'vm1') + create_ready_vm('pool1', 'vm2') + create_ready_vm('pool1', 'vm3') + create_ready_vm('pool2', 'vm4') + create_ready_vm('pool2', 'vm5') get '/dashboard/stats/vmpooler/pool', :history => true @@ -115,9 +113,7 @@ describe Vmpooler::API do expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.header['Content-Type']).to eq('application/json') end - end - end describe '/dashboard/stats/vmpooler/running' do @@ -129,7 +125,6 @@ describe Vmpooler::API do ], graphite: {} } } - let(:redis) { double('redis') } before do $config = config @@ -141,10 +136,6 @@ describe Vmpooler::API do context 'without history param' do it 'returns basic JSON' do - allow(redis).to receive(:scard) - - expect(redis).to receive(:scard).exactly(3).times - get '/dashboard/stats/vmpooler/running' json_hash = {pool: {running: 0}, diffpool: {running: 0}} @@ -155,9 +146,18 @@ describe Vmpooler::API do end it 'adds major correctly' do - allow(redis).to receive(:scard).with('vmpooler__running__pool-1').and_return(3) - allow(redis).to receive(:scard).with('vmpooler__running__pool-2').and_return(5) - allow(redis).to receive(:scard).with('vmpooler__running__diffpool-1').and_return(2) + create_running_vm('pool-1', 'vm1') + create_running_vm('pool-1', 'vm2') + create_running_vm('pool-1', 'vm3') + + create_running_vm('pool-2', 'vm4') + create_running_vm('pool-2', 'vm5') + create_running_vm('pool-2', 'vm6') + create_running_vm('pool-2', 'vm7') + create_running_vm('pool-2', 'vm8') + + create_running_vm('diffpool-1', 'vm9') + create_running_vm('diffpool-1', 'vm10') get '/dashboard/stats/vmpooler/running' @@ -167,10 +167,7 @@ describe Vmpooler::API do expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.header['Content-Type']).to eq('application/json') end - end end - end - end