From b65dca60a9e0d37ebc142c037218afcc24360e1a Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Fri, 20 Nov 2015 18:31:54 +0300 Subject: [PATCH] specs for several connectors --- devops-service/Gemfile | 2 +- devops-service/Gemfile.lock | 2 +- devops-service/db/mongo/connectors/filter.rb | 17 +- .../connectors/helpers/update_command.rb | 14 +- devops-service/db/mongo/connectors/project.rb | 20 +- devops-service/db/mongo/models/project.rb | 21 +- .../stack_template/stack_template_factory.rb | 2 + .../spec/connectors/filter_connector_spec.rb | 90 ++++++ .../spec/connectors/image_connector_spec.rb | 37 +++ .../spec/connectors/key_connector_spec.rb | 62 ++++ .../spec/connectors/project_connector_spec.rb | 295 ++++++++++++++++++ .../connectors/shared_connectors_context.rb | 22 ++ .../stack_template_connector_spec.rb | 42 +++ .../spec/connectors/tester_connector/base.rb | 53 ++++ .../connectors/tester_connector/filter.rb | 6 + .../spec/connectors/tester_connector/image.rb | 6 + .../spec/connectors/tester_connector/key.rb | 6 + .../connectors/tester_connector/project.rb | 6 + .../tester_connector/stack_template.rb | 6 + devops-service/spec/factories/key.rb | 1 + devops-service/spec/factories/project.rb | 12 + devops-service/spec/factories/report.rb | 4 +- .../models/deploy_env/deploy_env_ec2_spec.rb | 14 +- .../deploy_env/deploy_env_openstack_spec.rb | 14 +- devops-service/spec/models/project_spec.rb | 49 +++ devops-service/spec/models/server_spec.rb | 3 +- .../spec/models/shared_models_context.rb | 13 + devops-service/spec/spec_helper.rb | 8 + devops-service/spec/support/array_matcher.rb | 13 + .../spec/support/shared_connectors_specs.rb | 127 ++++++++ devops-service/spec/support/spec_support.rb | 8 + 31 files changed, 909 insertions(+), 66 deletions(-) create mode 100644 devops-service/spec/connectors/filter_connector_spec.rb create mode 100644 devops-service/spec/connectors/image_connector_spec.rb create mode 100644 devops-service/spec/connectors/key_connector_spec.rb create mode 100644 devops-service/spec/connectors/project_connector_spec.rb create mode 100644 devops-service/spec/connectors/shared_connectors_context.rb create mode 100644 devops-service/spec/connectors/stack_template_connector_spec.rb create mode 100644 devops-service/spec/connectors/tester_connector/base.rb create mode 100644 devops-service/spec/connectors/tester_connector/filter.rb create mode 100644 devops-service/spec/connectors/tester_connector/image.rb create mode 100644 devops-service/spec/connectors/tester_connector/key.rb create mode 100644 devops-service/spec/connectors/tester_connector/project.rb create mode 100644 devops-service/spec/connectors/tester_connector/stack_template.rb create mode 100644 devops-service/spec/factories/project.rb create mode 100644 devops-service/spec/models/project_spec.rb create mode 100644 devops-service/spec/models/shared_models_context.rb create mode 100644 devops-service/spec/support/array_matcher.rb create mode 100644 devops-service/spec/support/shared_connectors_specs.rb diff --git a/devops-service/Gemfile b/devops-service/Gemfile index 96840ba..13b88c3 100644 --- a/devops-service/Gemfile +++ b/devops-service/Gemfile @@ -10,7 +10,7 @@ gem "sinatra-websocket"#, "~>0.3.0" gem "fog", "~>1.20" gem "mixlib-shellout" gem "chef", ">=12" -gem "mongo" +gem "mongo", '1.12.3' gem "bson_ext" gem "multi_json", "1.7.8" gem "sidekiq", "3.2.6" diff --git a/devops-service/Gemfile.lock b/devops-service/Gemfile.lock index b5bf929..31da4a0 100644 --- a/devops-service/Gemfile.lock +++ b/devops-service/Gemfile.lock @@ -344,7 +344,7 @@ DEPENDENCIES httpclient mime-types (~> 1.25.1) mixlib-shellout - mongo + mongo (= 1.12.3) multi_json (= 1.7.8) rack (= 1.5.2) rack-accept-media-types diff --git a/devops-service/db/mongo/connectors/filter.rb b/devops-service/db/mongo/connectors/filter.rb index 8fde9c1..3bea129 100644 --- a/devops-service/db/mongo/connectors/filter.rb +++ b/devops-service/db/mongo/connectors/filter.rb @@ -20,23 +20,24 @@ module Connectors f = collection.find('type' => 'image', 'provider' => provider).to_a.first if f.nil? collection.insert('type' => 'image', 'provider' => provider, 'images' => images) - return images + images else f['images'] |= images collection.update({'_id' => f['_id']}, f) - return f['images'] + f['images'] end end def delete_available_images images, provider return unless images.is_a?(Array) - f = collection.find('type' => 'image', 'provider' => provider).to_a.first - unless f.nil? - f['images'] -= images - collection.update({'_id' => f['_id']}, f) - return f['images'] + filter = collection.find('type' => 'image', 'provider' => provider).to_a.first + if filter + filter['images'] -= images + collection.update({'_id' => filter['_id']}, filter) + filter['images'] + else + [] end - [] end end end diff --git a/devops-service/db/mongo/connectors/helpers/update_command.rb b/devops-service/db/mongo/connectors/helpers/update_command.rb index e1ebec7..674eb60 100644 --- a/devops-service/db/mongo/connectors/helpers/update_command.rb +++ b/devops-service/db/mongo/connectors/helpers/update_command.rb @@ -14,16 +14,10 @@ module Connectors end def update(record) - begin - record.validate! - collection.update({"_id" => record.id}, record.to_mongo_hash) - record - rescue Mongo::OperationFailure => e - if e.message =~ /^11000/ - resource_name = StringHelper.underscore_class(record.class) - raise InvalidRecord.new("Duplicate key error: #{resource_name} with id '#{record.id}'") - end - end + record.validate! + r = collection.update({"_id" => record.id}, record.to_mongo_hash) + raise RecordNotFound.new("'#{record.id}' not found") if r['n'] == 0 + record end end diff --git a/devops-service/db/mongo/connectors/project.rb b/devops-service/db/mongo/connectors/project.rb index 4f10fe2..abe74f9 100644 --- a/devops-service/db/mongo/connectors/project.rb +++ b/devops-service/db/mongo/connectors/project.rb @@ -2,9 +2,7 @@ module Connectors class Project < Base include Helpers::InsertCommand, Helpers::ShowCommand, -# Helpers::ListCommand, Helpers::DeleteCommand -# Helpers::UpdateCommand def initialize(db) @@ -73,7 +71,7 @@ module Connectors res.each do |ar| r[ar['_id']] = ar['envs'] end - return r + r end def projects_by_image(image) @@ -98,10 +96,9 @@ module Connectors project end + # {find all projects with deploy_envs with field=value}, {return deploy_envs with field=value} def projects_and_deploy_envs_by_field field, value - q = {} - q[field] = value - # {find all projects with deploy_envs with field=value}, {return deploy_envs with field=value} + q = {field => value} list( {'deploy_envs' => {'$elemMatch' => q}}, {:fields => {'deploy_envs' => {'$elemMatch' => q}}} ) end @@ -114,12 +111,12 @@ module Connectors end def set_project_env_run_list(project_id, env, run_list) - Helpers::RunList.new(run_list).validate! + ::Validators::Helpers::RunList.new(run_list).validate! set_project_deploy_env_field(project_id, env, {"run_list" => run_list}) end - def set_project_run_list(project_id, env, run_list) - Helpers::RunList.new(run_list).validate! + def set_project_run_list(project_id, run_list) + ::Validators::Helpers::RunList.new(run_list).validate! @collection.update({"_id" => project_id}, {"$set" => {run_list: run_list}}) end @@ -128,17 +125,16 @@ module Connectors end def remove_deploy_env_from_project id, env + raise ArgumentError unless env.is_a?(String) @collection.update({"_id" => id}, {'$pull' => {deploy_envs: {identifier: env}} }) end def project_update_field id, field, value - obj = {} - obj[field] = value + obj = {field => value} @collection.update({"_id" => id}, {'$set' => obj }) end def project_update id, params - #raise InvalidRecord.new("You can not change project name for '#{id}'.") if params["name"] keys = %w(run_list description) params.delete_if{|k,v| !keys.include?(k)} @collection.update({"_id" => id}, {'$set' => params }) diff --git a/devops-service/db/mongo/models/project.rb b/devops-service/db/mongo/models/project.rb index e34a779..8b14847 100644 --- a/devops-service/db/mongo/models/project.rb +++ b/devops-service/db/mongo/models/project.rb @@ -23,16 +23,27 @@ module Devops attr_accessor :id, :deploy_envs, :type, :archived, :description, :run_list - types :id => {:type => String, :empty => false}, - :deploy_envs => {:type => Array, :value_type => false, :empty => false}, - :description => {:type => String, :empty => true, :nil => true}, - :run_list => {:type => Array, :value_type => String, :empty => true, :nil => false} - MULTI_TYPE = "multi" + set_field_validators :id, [::Validators::FieldValidator::NotNil, + ::Validators::FieldValidator::FieldType::String, + ::Validators::FieldValidator::NotEmpty] + + set_field_validators :deploy_envs, [::Validators::FieldValidator::NotNil, + ::Validators::FieldValidator::FieldType::Array, + ::Validators::FieldValidator::NotEmpty] + + set_field_validators :description, [::Validators::FieldValidator::Nil, + ::Validators::FieldValidator::FieldType::String] + + set_field_validators :run_list, [::Validators::FieldValidator::NotNil, + ::Validators::FieldValidator::FieldType::Array, + ::Validators::FieldValidator::RunList] + set_validators ::Validators::DeployEnv::RunList, ::Validators::DeployEnv::DeployEnvs + def self.fields ["deploy_envs", "type", "description"] end diff --git a/devops-service/db/mongo/models/stack_template/stack_template_factory.rb b/devops-service/db/mongo/models/stack_template/stack_template_factory.rb index c085196..2094da3 100644 --- a/devops-service/db/mongo/models/stack_template/stack_template_factory.rb +++ b/devops-service/db/mongo/models/stack_template/stack_template_factory.rb @@ -1,6 +1,8 @@ require_relative "stack_template_base" require_relative "stack_template_openstack" require_relative "stack_template_ec2" +require 'providers/openstack' +require 'providers/ec2' module Devops module Model diff --git a/devops-service/spec/connectors/filter_connector_spec.rb b/devops-service/spec/connectors/filter_connector_spec.rb new file mode 100644 index 0000000..2850835 --- /dev/null +++ b/devops-service/spec/connectors/filter_connector_spec.rb @@ -0,0 +1,90 @@ +require 'db/mongo/connectors/filter' +require 'spec/connectors/tester_connector/filter' +require_relative 'shared_connectors_context' + +RSpec.describe Connectors::Filter, type: :connector do + set_tester_connector TesterConnector::Filter + include_context 'connectors' + let(:provider) {'ec2'} + + describe '#available_images' do + subject { @connector.available_images(provider) } + + context 'when there is no filter for given provider' do + it 'returns empty array' do + expect(subject).to eq [] + end + + context 'when there is filter for given provider but for another type' do + it 'returns empty array' do + test_filter = {provider: provider, type: 'foo', images: ['foo', 'bar']} + @tester_connector.create(test_filter) do + expect(subject).to eq [] + end + end + end + end + + context 'when there is filter for given provider' do + it "returns array with images' names" do + test_filter = {provider: provider, type: 'image', images: ['foo', 'bar']} + @tester_connector.create(test_filter) do + expect(subject).to match_array ['foo', 'bar'] + end + end + end + end + + describe '#add_available_images', cleanup_after: :each do + subject { @connector.add_available_images(['foo'], provider) } + + it "do nothing if images is not Array" do + expect { + result = @connector.add_available_images('foo', provider) + expect(result).to be_nil + }.not_to change {@tester_connector.list} + end + + it 'creates filter if there is no one' do + expect(@tester_connector.list.empty?).to be true + expect(subject).to match_array ['foo'] + expect(@tester_connector.list.empty?).to be false + end + + it 'updates filter if one already exists' do + @tester_connector.create(provider: provider, type: 'image', images: ['bar']) + expect(subject).to match_array ['bar', 'foo'] + expect(@tester_connector.list.first['images']).to match_array ['bar', 'foo'] + end + + it "doesn't duplicate images in filter" do + @tester_connector.create(provider: provider, type: 'image', images: ['foo']) + expect(subject).to match_array ['foo'] + expect(@tester_connector.list.first['images']).to match_array ['foo'] + end + end + + describe '#delete_available_images' do + subject { @connector.delete_available_images(['foo'], provider) } + after { @tester_connector.cleanup } + let(:create_filter) { @tester_connector.create(provider: provider, type: 'image', images: ['foo', 'bar']) } + + it 'does nothing if images is not Array' do + create_filter + expect { + result = @connector.delete_available_images('foo', provider) + expect(result).to be_nil + }.not_to change{ @tester_connector.list.first['images'] } + end + + it 'removes given images from filter' do + create_filter + expect(subject).to match_array ['bar'] + expect(@tester_connector.list.first['images']).to match_array ['bar'] + end + + it 'returns empty array if there is no filter' do + expect(subject).to eq [] + end + end +end diff --git a/devops-service/spec/connectors/image_connector_spec.rb b/devops-service/spec/connectors/image_connector_spec.rb new file mode 100644 index 0000000..585d728 --- /dev/null +++ b/devops-service/spec/connectors/image_connector_spec.rb @@ -0,0 +1,37 @@ +require 'db/mongo/connectors/image' +require 'spec/connectors/tester_connector/image' +require_relative 'shared_connectors_context' + +RSpec.describe Connectors::Image, type: :connector do + set_tester_connector TesterConnector::Image + include_context 'connectors' + let(:model_class) { Devops::Model::Image } + + include_examples 'mongo connector', { + model_name: :image, + only: [:insert, :show, :update, :delete], + field_to_update: :name + } + + describe '#images', cleanup_after: :each do + subject { @connector.images('ec2') } + + it 'should be empty if collection is empty' do + expect(subject).to eq [] + end + + it "returns array of Model::Key's of given provider" do + @tester_connector.create(provider: 'ec2') + @tester_connector.create(provider: 'openstack') + expect(subject).to be_an_array_of(model_class).and have_size(1) + expect(subject.first.provider).to eq 'ec2' + end + + it 'returns images for both providers if is is unset' do + @tester_connector.create(provider: 'ec2') + @tester_connector.create(provider: 'openstack') + expect(@connector.images.length).to eq 2 + end + end + +end diff --git a/devops-service/spec/connectors/key_connector_spec.rb b/devops-service/spec/connectors/key_connector_spec.rb new file mode 100644 index 0000000..9be46ee --- /dev/null +++ b/devops-service/spec/connectors/key_connector_spec.rb @@ -0,0 +1,62 @@ +require 'db/mongo/connectors/key' +require 'db/mongo/models/key' +require 'spec/connectors/tester_connector/key' +require_relative 'shared_connectors_context' + +RSpec.describe Connectors::Key, type: :connector do + set_tester_connector TesterConnector::Key + include_context 'connectors' + let(:model_class) { Devops::Model::Key } + + include_examples 'mongo connector', model_name: :key, only: [:insert, :list] + + describe '#key' do + context 'when scope was passed' do + subject { @connector.key('foo') } + + it 'raises RecordNotFound when there is no such record' do + expect { subject }.to raise_error(RecordNotFound) + end + + it 'returns record if was found' do + @tester_connector.create(id: 'foo') do + expect(subject).to be_an_instance_of(model_class) + end + end + end + + context 'when scope was passed' do + subject { @connector.key('foo', 'user') } + + it 'raises RecordNotFound when there is no such record' do + expect { subject }.to raise_error(RecordNotFound) + end + + it 'returns record if was found' do + @tester_connector.create(id: 'foo', scope: 'user') do + expect(subject).to be_an_instance_of(model_class) + end + end + end + end + + describe '#key_delete' do + subject { @connector.key_delete('foo') } + + it 'raises RecordNotFound if there is no such record' do + expect{ subject }.to raise_error(RecordNotFound) + end + + it 'remove key in "user" scope' do + @tester_connector.create(id: 'foo', scope: 'user') + subject + expect(@tester_connector.list).to eq [] + end + + it "doesn't remove key in 'system' scope" do + @tester_connector.create(id: 'foo', scope: 'system') do + expect {subject}.to raise_error RecordNotFound + end + end + end +end diff --git a/devops-service/spec/connectors/project_connector_spec.rb b/devops-service/spec/connectors/project_connector_spec.rb new file mode 100644 index 0000000..2b86bd8 --- /dev/null +++ b/devops-service/spec/connectors/project_connector_spec.rb @@ -0,0 +1,295 @@ +require 'db/mongo/connectors/project' +require 'spec/connectors/tester_connector/project' +require_relative 'shared_connectors_context' + +RSpec.describe Connectors::Project, type: :connector do + set_tester_connector TesterConnector::Project + include_context 'connectors' + let(:model_class) { Devops::Model::Project } + + include_examples 'mongo connector', { + model_name: :project, + factory_name: :project, + only: [:insert, :show, :delete], + field_to_update: :deploy_envs + } + + describe '#is_project_exists?' do + subject { @connector.is_project_exists?(build(:project, id: 'foo')) } + it 'returns true if project exists' do + @tester_connector.create(id: 'foo') do + expect(subject).to be true + end + end + it 'returns false if project doesn\'t exists' do + expect(subject).to be false + end + end + + describe '#projects_all' do + it 'returns array of projects' do + @tester_connector.create_list(2) do + result = @connector.projects_all + expect(result).to be_an_array_of(model_class).and have_size(2) + end + end + end + + describe '#projects', cleanup_after: :all do + before(:all) do + @tester_connector.create build(:project, id: 'foo', type: 'multi').to_mongo_hash + @tester_connector.create build(:project, id: 'bar', archived: true).to_mongo_hash + @tester_connector.create build(:project, id: 'baz').to_mongo_hash + end + + it 'returns non archived projects with all params unset' do + expect(@connector.projects).to have_size(2) + end + + it 'returns projects with given ids' do + result = @connector.projects(%w(foo baz)) + expect(result).to be_an_array_of(model_class) + expect(result.map(&:id)).to match_array(%w(foo baz)) + end + + it 'returns multi projects if @type == :multi' do + expect( + @connector.projects(nil, :multi).map(&:id) + ).to match_array ['foo'] + end + + it 'returns only given fields and id if @fields is set' do + result = @connector.projects(nil, nil, [:deploy_envs]) + expect(result.map(&:id).compact).not_to eq [] + expect(result.map(&:deploy_envs).compact).not_to eq [] + expect(result.map(&:description).compact).to eq [] + end + + it 'returns only archived projects if @archived=true' do + result = @connector.projects(nil, nil, [], true) + expect(result.map(&:id)).to match_array(%w(bar)) + end + end + + describe '#project_names_with_envs', cleanup_after: :all do + before(:all) do + @tester_connector.create( + id: 'foo', + deploy_envs: [{identifier: 'env1'}, {identifier: 'env2'}] + ) + @tester_connector.create( + id: 'bar', + deploy_envs: [{identifier: 'env3'}, {identifier: 'env4'}] + ) + end + + it 'returns hash like {"project_name" => ["env1", "env2]}' do + result = @connector.project_names_with_envs + expect(result.keys.sort).to match_array %w(foo bar) + expect(result['foo'].sort).to match_array %w(env1 env2) + expect(result['bar'].sort).to match_array %w(env3 env4) + end + + it 'returns only projects with given names' do + expect(@connector.project_names_with_envs(['bar']).keys).to match_array %w(bar) + end + end + + describe '#projects_by_image', cleanup_after: :each do + def env(image) + {image: image, provider: 'ec2'} + end + + it 'returns projects deploy_envs of which have given image' do + @tester_connector.create(id: 'foo', deploy_envs: [env('a'), env('b')]) + @tester_connector.create(id: 'bar', deploy_envs: [env('a'), env('a')]) + @tester_connector.create(id: 'baz', deploy_envs: [env('b'), env('b')]) + result = @connector.projects_by_image('a') + expect(result).to be_an_array_of(model_class) + expect(result.map(&:id)).to match_array %w(foo bar) + end + end + + describe '#projects_by_user', cleanup_after: :each do + def env(users) + {users: users, provider: 'ec2'} + end + + it 'returns projects deploy_envs of which have given user' do + @tester_connector.create(id: 'foo', deploy_envs: [ env(%w(user1 user2)) ]) + @tester_connector.create(id: 'bar', deploy_envs: [ env(%w(user1 user1)) ]) + @tester_connector.create(id: 'baz', deploy_envs: [ env(%w(user2 user2)) ]) + @tester_connector.create(id: 'baf', deploy_envs: [ env(%w(user1)), env(%w(user2)) ]) + result = @connector.projects_by_user('user1') + expect(result).to be_an_array_of(model_class) + expect(result.map(&:id)).to match_array %w(foo bar baf) + end + end + + describe '#archive_project' do + it 'sets archived to true' do + @tester_connector.create(id: 'foo') do + @connector.archive_project('foo') + expect(@tester_connector.show('foo')).to include('archived' => true) + end + end + end + + describe '#unarchive_project' do + it 'unsets archived' do + @tester_connector.create(id: 'foo', archived: true) do + @connector.unarchive_project('foo') + expect(@tester_connector.show('foo')).not_to include('archived') + end + end + end + + describe '#check_project_auth', cleanup_after: :all do + before(:all) do + @tester_connector.create(id: 'foo', deploy_envs: [ + {identifier: 'bar', provider: 'ec2', users: %w(user1)} + ]) + end + + it "raises InvalidPrivileges if given env users don't include given user" do + expect { + @connector.check_project_auth('foo', 'bar', 'user2') + }.to raise_error(InvalidPrivileges) + end + + it "returns project if env's users include given user" do + result = @connector.check_project_auth('foo', 'bar', 'user1') + expect(result).to be_an_instance_of(model_class) + expect(result.id).to eq 'foo' + end + end + + describe '#projects_and_deploy_envs_by_field', cleanup_after: :all do + def env(field, value) + {provider: 'ec2', field => value} + end + before(:all) do + @tester_connector.create(id: 'foo', deploy_envs: [ + env(:image, 'image1'), env(:stack_template, 'template1') + ]) + end + + it 'returns projects with deploy envs containing given field with given value' do + expect( + @connector.projects_and_deploy_envs_by_field(:image, 'image1').map(&:id) + ).to match_array %w(foo) + + expect( + @connector.projects_and_deploy_envs_by_field(:stack_template, 'template1').map(&:id) + ).to match_array %w(foo) + end + end + + describe '#set_project_deploy_env_field', cleanup_after: :each do + it 'updates given env from given field_value_hash' do + @tester_connector.create(id: 'foo', deploy_envs: [ + {identifier: 'bar', provider: 'ec2'} + ]) + @connector.set_project_deploy_env_field('foo', 'bar', image: 'a', stack_template: 'b') + updated_project = @tester_connector.show('foo') + expect(updated_project['deploy_envs'].first['image']).to eq 'a' + expect(updated_project['deploy_envs'].first['stack_template']).to eq 'b' + end + end + + describe '#set_project_env_run_list', cleanup_after: :all do + before(:all) do + @tester_connector.create(id: 'foo', deploy_envs: [{identifier: 'bar', provider: 'ec2'}]) + end + before { allow_any_instance_of(Validators::Helpers::RunList).to receive(:validate!) } + + it 'validates run list' do + run_list = [] + expect_any_instance_of(Validators::Helpers::RunList).to receive(:validate!) + @connector.set_project_env_run_list('foo', 'bar', run_list) + end + + it "updates env's RunList" do + run_list = ['role[foo]'] + @connector.set_project_env_run_list('foo', 'bar', run_list) + expect(@tester_connector.show('foo')['deploy_envs'].first['run_list']).to eq run_list + end + end + + describe '#set_project_env_run_list', cleanup_after: :all do + before(:all) do + @tester_connector.create(id: 'foo') + end + before { allow_any_instance_of(Validators::Helpers::RunList).to receive(:validate!) } + + it 'validates run list' do + run_list = [] + expect_any_instance_of(Validators::Helpers::RunList).to receive(:validate!) + @connector.set_project_run_list('foo', run_list) + end + + it "updates project's RunList" do + run_list = ['role[foo]'] + @connector.set_project_run_list('foo', run_list) + expect(@tester_connector.show('foo')['run_list']).to eq run_list + end + end + + describe '#add_deploy_env_to_project' do + it 'adds env to project' do + @tester_connector.create(id: 'foo', deploy_envs: []) do + @connector.add_deploy_env_to_project('foo', build(:deploy_env)) + expect(@tester_connector.show('foo')['deploy_envs']).not_to be_empty + end + end + end + + describe '#remove_deploy_env_from_project', cleanup_after: :each do + before { @tester_connector.create(id: 'foo', deploy_envs: [{identifier: 'bar'}]) } + + it "removes env from project" do + @connector.remove_deploy_env_from_project('foo', 'bar') + expect(@tester_connector.show('foo')['deploy_envs']).to be_empty + end + + it 'raises ArgumentError if given env is not a String' do + env = build(:deploy_env_ec2, identifier: 'bar') + expect { @connector.remove_deploy_env_from_project('foo', env) }.to raise_error(ArgumentError) + end + end + + describe '#project_update_field', cleanup_after: :each do + before { @tester_connector.create(id: 'foo', description: 'desc', run_list: []) } + subject { @connector.project_update_field('foo', 'run_list', ['role[a]']) } + + it 'updates given field of given project' do + expect {subject}.to change { @tester_connector.show('foo')['run_list'] }.to ['role[a]'] + end + + it "doesn't affect other fields" do + expect {subject}.not_to change { @tester_connector.show('foo')['desc'] } + end + end + + describe '#project_update', cleanup_after: :each do + before { @tester_connector.create(id: 'foo', deploy_envs: [], description: 'desc', run_list: []) } + subject { + attrs = {'id' => 'foo2', 'deploy_envs' => [build(:deploy_env)], 'description' => 'desc2', 'run_list' => ['role[asd]']} + @connector.project_update('foo', attrs) + } + let(:updated_project) { @tester_connector.show('foo') } + + it 'can update run_list and description' do + subject + expect(updated_project['run_list']).to eq ['role[asd]'] + expect(updated_project['description']).to eq 'desc2' + end + + it 'can not update other fields' do + subject + expect(updated_project['_id']).to eq 'foo' + expect(updated_project['deploy_envs']).to eq [] + end + end + +end diff --git a/devops-service/spec/connectors/shared_connectors_context.rb b/devops-service/spec/connectors/shared_connectors_context.rb new file mode 100644 index 0000000..3c0fedb --- /dev/null +++ b/devops-service/spec/connectors/shared_connectors_context.rb @@ -0,0 +1,22 @@ +def set_tester_connector(klass) + define_method :tester_connector_class do + klass + end +end + +RSpec.shared_context 'connectors' do + before(:all) do + @connector = described_class.new(SpecSupport.db) + @tester_connector = tester_connector_class.new + @tester_connector.cleanup + end + after(:all){ @tester_connector.cleanup } +end + +RSpec.shared_context 'cleanup after all', cleanup_after: :all do + after(:all){ @tester_connector.cleanup } +end + +RSpec.shared_context 'cleanup after each', cleanup_after: :each do + after { @tester_connector.cleanup } +end \ No newline at end of file diff --git a/devops-service/spec/connectors/stack_template_connector_spec.rb b/devops-service/spec/connectors/stack_template_connector_spec.rb new file mode 100644 index 0000000..edb9fe7 --- /dev/null +++ b/devops-service/spec/connectors/stack_template_connector_spec.rb @@ -0,0 +1,42 @@ +require 'db/mongo/connectors/stack_template' +require 'spec/connectors/tester_connector/stack_template' +require_relative 'shared_connectors_context' + +RSpec.describe Connectors::StackTemplate, type: :connector do + set_tester_connector TesterConnector::StackTemplate + include_context 'connectors' + let(:model_class) { Devops::Model::StackTemplateEc2 } + + include_examples 'mongo connector', { + model_name: :stack_template, + factory_name: :stack_template_ec2, + only: [:insert, :show, :update, :delete], + field_to_update: :owner + } + + describe '#stack_templates' do + subject { @connector.stack_templates('ec2') } + + it 'should be empty if collection is empty' do + expect(subject).to eq [] + end + + it "returns array of stack_templates of given provider" do + @tester_connector.create(provider: 'ec2') + @tester_connector.create(provider: 'openstack') + expect(subject).to be_a(Array) + expect(subject.length).to eq 1 + expect(subject.first).to be_an_instance_of(model_class) + expect(subject.first.provider).to eq 'ec2' + @tester_connector.cleanup + end + + it 'returns stack_templates for both providers if is is unset' do + @tester_connector.create(provider: 'ec2') + @tester_connector.create(provider: 'openstack') + expect(@connector.stack_templates.length).to eq 2 + @tester_connector.cleanup + end + end + +end diff --git a/devops-service/spec/connectors/tester_connector/base.rb b/devops-service/spec/connectors/tester_connector/base.rb new file mode 100644 index 0000000..4f595ab --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/base.rb @@ -0,0 +1,53 @@ +module TesterConnector + class Base + attr_reader :collection + + def initialize + collection_name = self.class.name.demodulize.underscore.pluralize + @collection = SpecSupport.db.collection(collection_name) + @next_id = 1 + end + + def create(hash={}) + collection.insert(create_params(hash)) + if block_given? + yield + cleanup + end + end + + def create_list(size=2, hash={}) + size.times { create(hash) } + if block_given? + yield + cleanup + end + end + + def list + collection.find().to_a + end + + def show(id) + collection.find({'_id' => id}).to_a.first + end + + def cleanup + collection.remove + end + + private + + def create_params(hash) + params = hash.dup + if params[:id] + params['_id'] = params.delete(:id) + end + unless params['_id'] + params['_id'] = @next_id + @next_id += 1 + end + params + end + end +end \ No newline at end of file diff --git a/devops-service/spec/connectors/tester_connector/filter.rb b/devops-service/spec/connectors/tester_connector/filter.rb new file mode 100644 index 0000000..c8cd04f --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/filter.rb @@ -0,0 +1,6 @@ +require_relative 'base' + +module TesterConnector + class Filter < Base + end +end \ No newline at end of file diff --git a/devops-service/spec/connectors/tester_connector/image.rb b/devops-service/spec/connectors/tester_connector/image.rb new file mode 100644 index 0000000..5f7ac86 --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/image.rb @@ -0,0 +1,6 @@ +require_relative 'base' + +module TesterConnector + class Image < Base + end +end \ No newline at end of file diff --git a/devops-service/spec/connectors/tester_connector/key.rb b/devops-service/spec/connectors/tester_connector/key.rb new file mode 100644 index 0000000..f9dd014 --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/key.rb @@ -0,0 +1,6 @@ +require_relative 'base' + +module TesterConnector + class Key < Base + end +end \ No newline at end of file diff --git a/devops-service/spec/connectors/tester_connector/project.rb b/devops-service/spec/connectors/tester_connector/project.rb new file mode 100644 index 0000000..36f45e2 --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/project.rb @@ -0,0 +1,6 @@ +require_relative 'base' + +module TesterConnector + class Project < Base + end +end \ No newline at end of file diff --git a/devops-service/spec/connectors/tester_connector/stack_template.rb b/devops-service/spec/connectors/tester_connector/stack_template.rb new file mode 100644 index 0000000..38383aa --- /dev/null +++ b/devops-service/spec/connectors/tester_connector/stack_template.rb @@ -0,0 +1,6 @@ +require_relative 'base' + +module TesterConnector + class StackTemplate < Base + end +end \ No newline at end of file diff --git a/devops-service/spec/factories/key.rb b/devops-service/spec/factories/key.rb index 48b0262..06fb96e 100644 --- a/devops-service/spec/factories/key.rb +++ b/devops-service/spec/factories/key.rb @@ -2,6 +2,7 @@ require 'db/mongo/models/key' FactoryGirl.define do factory :key, class: Devops::Model::Key do + id 'user_key' path SpecSupport::BLANK_FILE scope 'user' end diff --git a/devops-service/spec/factories/project.rb b/devops-service/spec/factories/project.rb new file mode 100644 index 0000000..561085f --- /dev/null +++ b/devops-service/spec/factories/project.rb @@ -0,0 +1,12 @@ +require 'db/mongo/models/project' + +FactoryGirl.define do + factory :project, class: Devops::Model::Project do + id 'my_project' + deploy_envs { + [build(:deploy_env_ec2)] + } + run_list [] + description 'desc' + end +end \ No newline at end of file diff --git a/devops-service/spec/factories/report.rb b/devops-service/spec/factories/report.rb index 70395a5..07711f0 100644 --- a/devops-service/spec/factories/report.rb +++ b/devops-service/spec/factories/report.rb @@ -13,8 +13,6 @@ FactoryGirl.define do updated_at Time.now status 'completed' - initialize_with { - new(attributes.stringify_keys) - } + initialize_with { new(attributes.stringify_keys) } end end \ No newline at end of file diff --git a/devops-service/spec/models/deploy_env/deploy_env_ec2_spec.rb b/devops-service/spec/models/deploy_env/deploy_env_ec2_spec.rb index 5b622b9..2d3b2d8 100644 --- a/devops-service/spec/models/deploy_env/deploy_env_ec2_spec.rb +++ b/devops-service/spec/models/deploy_env/deploy_env_ec2_spec.rb @@ -1,19 +1,9 @@ require 'db/mongo/models/deploy_env/deploy_env_ec2' +require_relative '../shared_models_context' RSpec.describe Devops::Model::DeployEnvEc2, type: :model do let(:env) { build(:deploy_env_ec2) } - - before do - allow(Provider::ProviderFactory).to receive(:providers).and_return(%w(ec2)) - allow_any_instance_of(Validators::Helpers::Users).to receive(:available_users).and_return(['root']) - allow_any_instance_of(Validators::DeployEnv::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) - allow_any_instance_of(Validators::FieldValidator::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) - allow_any_instance_of(Validators::DeployEnv::Groups).to receive(:available_groups).and_return(['default']) - allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - allow_any_instance_of(Validators::DeployEnv::StackTemplate).to receive(:available_stack_templates).and_return([{'id' => 'template'}]) - allow_any_instance_of(Validators::FieldValidator::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - end + include_context 'stubbed calls to connector in env validators' it_behaves_like 'deploy env' it_behaves_like 'cloud deploy env' diff --git a/devops-service/spec/models/deploy_env/deploy_env_openstack_spec.rb b/devops-service/spec/models/deploy_env/deploy_env_openstack_spec.rb index 428241f..08783b6 100644 --- a/devops-service/spec/models/deploy_env/deploy_env_openstack_spec.rb +++ b/devops-service/spec/models/deploy_env/deploy_env_openstack_spec.rb @@ -1,19 +1,9 @@ require 'db/mongo/models/deploy_env/deploy_env_openstack' +require_relative '../shared_models_context' RSpec.describe Devops::Model::DeployEnvOpenstack, type: :model do let(:env) { build(:deploy_env_openstack) } - - before do - allow(Provider::ProviderFactory).to receive(:providers).and_return(%w(openstack)) - allow_any_instance_of(Validators::Helpers::Users).to receive(:available_users).and_return(['root']) - allow_any_instance_of(Validators::DeployEnv::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) - allow_any_instance_of(Validators::FieldValidator::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) - allow_any_instance_of(Validators::DeployEnv::Groups).to receive(:available_groups).and_return(['default']) - allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - allow_any_instance_of(Validators::DeployEnv::StackTemplate).to receive(:available_stack_templates).and_return([{'id' => 'template'}]) - allow_any_instance_of(Validators::FieldValidator::Image).to receive(:available_images).and_return([{'id' => 'image'}]) - end + include_context 'stubbed calls to connector in env validators' it_behaves_like 'deploy env' it_behaves_like 'cloud deploy env' diff --git a/devops-service/spec/models/project_spec.rb b/devops-service/spec/models/project_spec.rb new file mode 100644 index 0000000..9b19eb2 --- /dev/null +++ b/devops-service/spec/models/project_spec.rb @@ -0,0 +1,49 @@ +require 'db/mongo/models/project' +require_relative 'shared_models_context' + +RSpec.describe Devops::Model::Project, type: :model do + let(:project) { build(:project) } + include_context 'stubbed calls to connector in env validators' + + describe 'validation rules:' do + include_examples 'field type validation', :id, :not_nil, :non_empty_string + include_examples 'field type validation', :deploy_envs, :not_nil, :non_empty_array + include_examples 'field type validation', :description, :maybe_nil, :maybe_empty_string + include_examples 'field type validation', :run_list, :not_nil, :maybe_empty_array, :run_list + + it "isn't valid when has envs with same identifier" do + project.deploy_envs << build(:deploy_env_ec2) + expect(project).not_to be_valid + end + + it "is valid when all envs have uniq identifiers" do + project.deploy_envs << build(:deploy_env_ec2, identifier: 'new') + expect(project).to be_valid + end + + it "isn't valid when at least one of envs isn't valid" do + project.deploy_envs << build(:deploy_env_ec2, identifier: nil) + expect(project).not_to be_valid + end + end + + describe '.fields' do + subject { described_class.fields } + it { should eq %w(deploy_envs type description) } + end + + describe '#initialize' do + it 'sets @type to generic by default' do + expect(described_class.new.type).to eq 'generic' + end + + it 'sets @archived to false by default' do + expect(described_class.new.archived).to eq false + end + + it 'sets run_list to empty_array by default' do + expect(described_class.new.run_list).to eq [] + end + end + +end \ No newline at end of file diff --git a/devops-service/spec/models/server_spec.rb b/devops-service/spec/models/server_spec.rb index f970df8..d49f328 100644 --- a/devops-service/spec/models/server_spec.rb +++ b/devops-service/spec/models/server_spec.rb @@ -24,8 +24,7 @@ RSpec.describe Devops::Model::Server, type: :model do include_examples 'field type validation', :chef_node_name, :not_nil, :maybe_empty_string include_examples 'field type validation', :reserved_by, :not_nil, :maybe_empty_string include_examples 'field type validation', :stack, :maybe_nil, :non_empty_string - include_examples 'field type validation', :run_list, :not_nil, :maybe_empty_array - include_examples 'field type validation', :run_list, :run_list + include_examples 'field type validation', :run_list, :not_nil, :maybe_empty_array, :run_list end describe '#initialize' do diff --git a/devops-service/spec/models/shared_models_context.rb b/devops-service/spec/models/shared_models_context.rb new file mode 100644 index 0000000..51804e0 --- /dev/null +++ b/devops-service/spec/models/shared_models_context.rb @@ -0,0 +1,13 @@ +RSpec.shared_context 'stubbed calls to connector in env validators' do + before do + allow(Provider::ProviderFactory).to receive(:providers).and_return(%w(ec2 openstack)) + allow_any_instance_of(Validators::Helpers::Users).to receive(:available_users).and_return(['root']) + allow_any_instance_of(Validators::DeployEnv::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) + allow_any_instance_of(Validators::FieldValidator::Flavor).to receive(:available_flavors).and_return([{'id' => 'flavor'}]) + allow_any_instance_of(Validators::DeployEnv::Groups).to receive(:available_groups).and_return(['default']) + allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) + allow_any_instance_of(Validators::DeployEnv::Image).to receive(:available_images).and_return([{'id' => 'image'}]) + allow_any_instance_of(Validators::DeployEnv::StackTemplate).to receive(:available_stack_templates).and_return([{'id' => 'template'}]) + allow_any_instance_of(Validators::FieldValidator::Image).to receive(:available_images).and_return([{'id' => 'image'}]) + end +end diff --git a/devops-service/spec/spec_helper.rb b/devops-service/spec/spec_helper.rb index 7552bf9..ac74d92 100644 --- a/devops-service/spec/spec_helper.rb +++ b/devops-service/spec/spec_helper.rb @@ -8,6 +8,10 @@ require 'active_support/inflector' root = File.join(File.dirname(__FILE__), "..") $LOAD_PATH.push root unless $LOAD_PATH.include? root +# suppress output +original_stdout = $stdout +$stdout = File.open(File::NULL, "w") + Dir[("./spec/support/**/*.rb")].each { |f| require f } # Factory girl configuration @@ -21,6 +25,10 @@ FactoryGirl.find_definitions RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods + config.after(:all) do + $stdout = original_stdout + end + # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. diff --git a/devops-service/spec/support/array_matcher.rb b/devops-service/spec/support/array_matcher.rb new file mode 100644 index 0000000..34f7376 --- /dev/null +++ b/devops-service/spec/support/array_matcher.rb @@ -0,0 +1,13 @@ +RSpec::Matchers.define :be_an_array_of do |klass| + match do |actual| + actual.class == Array && + actual.all? {|item| item.class == klass} + end +end + +RSpec::Matchers.define :have_size do |length| + match { |actual| actual.length == length } + failure_message do |actual| + "expected #{actual} to have size #{expected} but have #{actual.length}" + end +end \ No newline at end of file diff --git a/devops-service/spec/support/shared_connectors_specs.rb b/devops-service/spec/support/shared_connectors_specs.rb new file mode 100644 index 0000000..2bfe99d --- /dev/null +++ b/devops-service/spec/support/shared_connectors_specs.rb @@ -0,0 +1,127 @@ +RSpec.shared_examples 'mongo connector' do |options| + model_name = options.fetch(:model_name) + factory_name = options[:factory_name] || model_name + commands = options.fetch(:only) + + if commands.include?(:insert) + insert_method_name = "#{model_name}_insert" + + describe "##{insert_method_name}" do + let(:model) { build(factory_name) } + subject { @connector.send(insert_method_name, model) } + + before { allow(model).to receive(:validate!).and_return(true) } + after { @tester_connector.cleanup } + + it 'inserts new record' do + expect {subject}.to change {@tester_connector.list.size}.from(0).to(1) + end + + it 'validates inserted record' do + expect(model).to receive(:validate!) + subject + end + + it "doesn't insert nonvalid records" do + allow(model).to receive(:validate!) { raise InvalidRecord.new('') } + expect {subject}.to raise_error(InvalidRecord) + end + + it 'sets created_at of record' do + expect(model.created_at).to be nil + subject + expect(@tester_connector.list.first['created_at']).not_to be nil + end + + it 'raises error if record with such id already exists' do + @tester_connector.create(id: model.id) + expect {subject}.to raise_error(InvalidRecord) + end + end + end + + if commands.include?(:list) + list_method_name = "#{model_name.to_s.pluralize}" + + describe "##{list_method_name}" do + subject { @connector.send(list_method_name) } + + it 'should be empty if collection is empty' do + expect(subject).to eq [] + end + + it "returns array of #{model_name.to_s.pluralize} if collection isn't empty" do + @tester_connector.create_list(2) do + expect(subject).to be_an_array_of(model_class).and have_size(2) + end + end + end + end + + if commands.include?(:show) + show_method_name = model_name + + describe "##{show_method_name}" do + subject { @connector.send(show_method_name, 'foo') } + + it 'raises RecordNotFound when there is no such record' do + expect { subject }.to raise_error(RecordNotFound) + end + + it 'returns record if was found' do + @tester_connector.create(build(factory_name, id: 'foo').to_mongo_hash) do + expect(subject).to be_an_instance_of(model_class) + end + end + end + end + + if commands.include?(:update) + update_method_name = "#{model_name}_update" + field_to_update = options.fetch(:field_to_update).to_s + + describe "##{update_method_name}" do + let(:model) { build(model_name, field_to_update => 'new_value') } + let(:insert_model_to_update) { @tester_connector.create(attributes_for(factory_name)) } + subject { @connector.send(update_method_name, model) } + before { allow(model).to receive(:validate!).and_return(true) } + after { @tester_connector.cleanup } + + it 'validates updated record' do + insert_model_to_update + expect(model).to receive(:validate!) + subject + end + + it 'updates record' do + insert_model_to_update + subject + updated_value = @tester_connector.list.first[field_to_update] + expect(updated_value).to eq 'new_value' + end + + it 'raises RecordNotFound if there is no such record' do + expect{ subject }.to raise_error(RecordNotFound) + end + end + end + + if commands.include?(:delete) + delete_method_name = "#{model_name}_delete" + describe "##{delete_method_name}" do + let(:model) { build(factory_name) } + let(:insert_model_to_delete) { @tester_connector.create(model.to_mongo_hash) } + subject { @connector.send(delete_method_name, model.id) } + + it 'deletes record' do + insert_model_to_delete + expect {subject}.to change {@tester_connector.list.empty?}.to(true) + @tester_connector.cleanup + end + + it 'raises RecordNotFound if there is no such record' do + expect { subject }.to raise_error(RecordNotFound) + end + end + end +end diff --git a/devops-service/spec/support/spec_support.rb b/devops-service/spec/support/spec_support.rb index 159e1cf..2c5a04e 100644 --- a/devops-service/spec/support/spec_support.rb +++ b/devops-service/spec/support/spec_support.rb @@ -2,10 +2,18 @@ require 'core/devops-application' module SpecSupport BLANK_FILE = File.join(Devops::Application.root, 'spec/support/blank_file') + TEST_DB = 'devops_test' def stub_loggers allow(DevopsLogger).to receive_message_chain('logger.debug') allow(DevopsLogger).to receive_message_chain('logger.info') allow(DevopsLogger).to receive_message_chain('logger.error') end + + def self.db + @db ||= begin + require 'mongo' + Mongo::MongoClient.new.db(TEST_DB) + end + end end \ No newline at end of file