diff options
Diffstat (limited to 'spec')
32 files changed, 756 insertions, 1281 deletions
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb index d3c8bf9d54f..b2ded945738 100644 --- a/spec/factories/ci/stages.rb +++ b/spec/factories/ci/stages.rb @@ -15,4 +15,12 @@ FactoryGirl.define do warnings: warnings) end end + + factory :ci_stage_entity, class: Ci::Stage do + project factory: :project + pipeline factory: :ci_empty_pipeline + + name 'test' + status 'pending' + end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 4a2034b31b3..9ebda0ba03b 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -81,6 +81,10 @@ FactoryGirl.define do archived true end + trait :hashed do + storage_version Project::LATEST_STORAGE_VERSION + end + trait :access_requestable do request_access_enabled true end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 79069bbca8e..9ce687afb31 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -41,6 +41,8 @@ describe "User Feed" do target_project: project, description: "Here is the fix: ![an image](image.png)") end + let(:push_event) { create(:push_event, project: project, author: user) } + let!(:push_event_payload) { create(:push_event_payload, event: push_event) } before do project.team << [user, :master] @@ -70,6 +72,10 @@ describe "User Feed" do it 'has XHTML summaries in merge request descriptions' do expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ end + + it 'has push event commit ID' do + expect(body).to have_content(Commit.truncate_sha(push_event.commit_id)) + end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index e59a484d992..20f9818b08b 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -104,18 +104,15 @@ feature 'Group' do end context 'as group owner' do - let(:user) { create(:user) } + it 'creates a nested group' do + user = create(:user) - before do group.add_owner(user) sign_out(:user) sign_in(user) visit subgroups_group_path(group) click_link 'New Subgroup' - end - - it 'creates a nested group' do fill_in 'Group path', with: 'bar' click_button 'Create group' @@ -123,6 +120,16 @@ feature 'Group' do expect(page).to have_content("Group 'bar' was successfully created.") end end + + context 'when nested group feature is disabled' do + it 'renders 404' do + allow(Group).to receive(:supports_nested_groups?).and_return(false) + + visit subgroups_group_path(group) + + expect(page.status_code).to eq(404) + end + end end it 'checks permissions to avoid exposing groups by parent_id' do diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index aa138f25bd3..4b72dbb7964 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -62,6 +62,12 @@ describe EventsHelper do expect(helper.event_note(input)).to eq(expected) end + it 'preserves data-src for lazy images' do + input = "![ImageTest](/uploads/test.png)" + image_url = "data-src=\"/uploads/test.png\"" + expect(helper.event_note(input)).to match(image_url) + end + context 'labels formatting' do let(:input) { 'this should be ~label_1' } diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml index 54bc1a59279..432cd5fcc74 100644 --- a/spec/javascripts/fixtures/project_select_combo_button.html.haml +++ b/spec/javascripts/fixtures/project_select_combo_button.html.haml @@ -1,6 +1,6 @@ .project-item-select-holder %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } } - %a.new-project-item-link{ data: { label: 'New issue' }, href: ''} + %a.new-project-item-link{ data: { label: 'New issue', type: 'issues' }, href: ''} %i.fa.fa-spinner.spin %a.new-project-item-select-button %i.fa.fa-caret-down diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js index e10a5a3bef6..021804e0769 100644 --- a/spec/javascripts/project_select_combo_button_spec.js +++ b/spec/javascripts/project_select_combo_button_spec.js @@ -101,5 +101,40 @@ describe('Project Select Combo Button', function () { window.localStorage.clear(); }); }); + + describe('deriveTextVariants', function () { + beforeEach(function () { + this.mockExecutionContext = { + resourceType: '', + resourceLabel: '', + }; + + this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); + + this.method = this.comboButton.deriveTextVariants.bind(this.mockExecutionContext); + }); + + it('correctly derives test variants for merge requests', function () { + this.mockExecutionContext.resourceType = 'merge_requests'; + this.mockExecutionContext.resourceLabel = 'New merge request'; + + const returnedVariants = this.method(); + + expect(returnedVariants.localStorageItemType).toBe('new-merge-request'); + expect(returnedVariants.defaultTextPrefix).toBe('New merge request'); + expect(returnedVariants.presetTextSuffix).toBe('merge request'); + }); + + it('correctly derives text variants for issues', function () { + this.mockExecutionContext.resourceType = 'issues'; + this.mockExecutionContext.resourceLabel = 'New issue'; + + const returnedVariants = this.method(); + + expect(returnedVariants.localStorageItemType).toBe('new-issue'); + expect(returnedVariants.defaultTextPrefix).toBe('New issue'); + expect(returnedVariants.presetTextSuffix).toBe('issue'); + }); + }); }); diff --git a/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb new file mode 100644 index 00000000000..878158910be --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MigrateStageStatus, :migration, schema: 20170711145320 do + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:stages) { table(:ci_stages) } + let(:jobs) { table(:ci_builds) } + + STATUSES = { created: 0, pending: 1, running: 2, success: 3, + failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + + before do + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') + pipelines.create!(id: 1, project_id: 1, ref: 'master', sha: 'adf43c3a') + stages.create!(id: 1, pipeline_id: 1, project_id: 1, name: 'test', status: nil) + stages.create!(id: 2, pipeline_id: 1, project_id: 1, name: 'deploy', status: nil) + end + + context 'when stage status is known' do + before do + create_job(project: 1, pipeline: 1, stage: 'test', status: 'success') + create_job(project: 1, pipeline: 1, stage: 'test', status: 'running') + create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'failed') + end + + it 'sets a correct stage status' do + described_class.new.perform(1, 2) + + expect(stages.first.status).to eq STATUSES[:running] + expect(stages.second.status).to eq STATUSES[:failed] + end + end + + context 'when stage status is not known' do + it 'sets a skipped stage status' do + described_class.new.perform(1, 2) + + expect(stages.first.status).to eq STATUSES[:skipped] + expect(stages.second.status).to eq STATUSES[:skipped] + end + end + + context 'when stage status includes status of a retried job' do + before do + create_job(project: 1, pipeline: 1, stage: 'test', status: 'canceled') + create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'failed', retried: true) + create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'success') + end + + it 'sets a correct stage status' do + described_class.new.perform(1, 2) + + expect(stages.first.status).to eq STATUSES[:canceled] + expect(stages.second.status).to eq STATUSES[:success] + end + end + + context 'when some job in the stage is blocked / manual' do + before do + create_job(project: 1, pipeline: 1, stage: 'test', status: 'failed') + create_job(project: 1, pipeline: 1, stage: 'test', status: 'manual') + create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'success', when: 'manual') + end + + it 'sets a correct stage status' do + described_class.new.perform(1, 2) + + expect(stages.first.status).to eq STATUSES[:manual] + expect(stages.second.status).to eq STATUSES[:success] + end + end + + def create_job(project:, pipeline:, stage:, status:, **opts) + stages = { test: 1, build: 2, deploy: 3 } + + jobs.create!(project_id: project, commit_id: pipeline, + stage_idx: stages[stage.to_sym], stage: stage, + status: status, **opts) + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a3ae0a4686d..8ec8dfe6acf 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -235,18 +235,10 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to be < 2 } end - describe '#has_commits?' do - it { expect(repository.has_commits?).to be_truthy } - end - describe '#empty?' do it { expect(repository.empty?).to be_falsey } end - describe '#bare?' do - it { expect(repository.bare?).to be_truthy } - end - describe '#ref_names' do let(:ref_names) { repository.ref_names } subject { ref_names } @@ -441,15 +433,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_names" do - let(:remotes) { repository.remote_names } - - it "should have one entry: 'origin'" do - expect(remotes.size).to eq(1) - expect(remotes.first).to eq("origin") - end - end - describe "#refs_hash" do let(:refs) { repository.refs_hash } diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 7fe698fcb18..2eaf4222964 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -111,6 +111,20 @@ describe Gitlab::GitalyClient::CommitService do client.tree_entries(repository, revision, path) end + + context 'with UTF-8 params strings' do + let(:revision) { "branch\u011F" } + let(:path) { "foo/\u011F.txt" } + + it 'handles string encodings correctly' do + expect_any_instance_of(Gitaly::CommitService::Stub) + .to receive(:get_tree_entries) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) + + client.tree_entries(repository, revision, path) + end + end end describe '#find_commit' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ae3b0173160..a5e03e149a7 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -227,6 +227,8 @@ Ci::Pipeline: Ci::Stage: - id - name +- status +- lock_version - project_id - pipeline_id - created_at diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb index 6186cec2689..b0b4fdc09bc 100644 --- a/spec/lib/gitlab/job_waiter_spec.rb +++ b/spec/lib/gitlab/job_waiter_spec.rb @@ -1,30 +1,39 @@ require 'spec_helper' describe Gitlab::JobWaiter do - describe '#wait' do - let(:waiter) { described_class.new(%w(a)) } - it 'returns when all jobs have been completed' do - expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a)) - .and_return(true) + describe '.notify' do + it 'pushes the jid to the named queue' do + key = 'gitlab:job_waiter:foo' + jid = 1 - expect(waiter).not_to receive(:sleep) + redis = double('redis') + expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) + expect(redis).to receive(:lpush).with(key, jid) - waiter.wait + described_class.notify(key, jid) end + end + + describe '#wait' do + let(:waiter) { described_class.new(2) } - it 'sleeps between checking the job statuses' do - expect(Gitlab::SidekiqStatus).to receive(:all_completed?) - .with(%w(a)) - .and_return(false, true) + it 'returns when all jobs have been completed' do + described_class.notify(waiter.key, 'a') + described_class.notify(waiter.key, 'b') - expect(waiter).to receive(:sleep).with(described_class::INTERVAL) + result = nil + expect { Timeout.timeout(1) { result = waiter.wait(2) } }.not_to raise_error - waiter.wait + expect(result).to contain_exactly('a', 'b') end - it 'returns when timing out' do - expect(waiter).not_to receive(:sleep) - waiter.wait(0) + it 'times out if not all jobs complete' do + described_class.notify(waiter.key, 'a') + + result = nil + expect { Timeout.timeout(2) { result = waiter.wait(1) } }.not_to raise_error + + expect(result).to contain_exactly('a') end end end diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb index 6374ac80207..2dbb7bb7c34 100644 --- a/spec/lib/gitlab/sidekiq_throttler_spec.rb +++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb @@ -1,28 +1,44 @@ require 'spec_helper' describe Gitlab::SidekiqThrottler do - before do - Sidekiq.options[:concurrency] = 35 - - stub_application_setting( - sidekiq_throttling_enabled: true, - sidekiq_throttling_factor: 0.1, - sidekiq_throttling_queues: %w[build project_cache] - ) - end - describe '#execute!' do - it 'sets limits on the selected queues' do - described_class.execute! + context 'when job throttling is enabled' do + before do + Sidekiq.options[:concurrency] = 35 + + stub_application_setting( + sidekiq_throttling_enabled: true, + sidekiq_throttling_factor: 0.1, + sidekiq_throttling_queues: %w[build project_cache] + ) + end + + it 'requires sidekiq-limit_fetch' do + expect(described_class).to receive(:require).with('sidekiq-limit_fetch').and_call_original + + described_class.execute! + end + + it 'sets limits on the selected queues' do + described_class.execute! + + expect(Sidekiq::Queue['build'].limit).to eq 4 + expect(Sidekiq::Queue['project_cache'].limit).to eq 4 + end + + it 'does not set limits on other queues' do + described_class.execute! - expect(Sidekiq::Queue['build'].limit).to eq 4 - expect(Sidekiq::Queue['project_cache'].limit).to eq 4 + expect(Sidekiq::Queue['merge'].limit).to be_nil + end end - it 'does not set limits on other queues' do - described_class.execute! + context 'when job throttling is disabled' do + it 'does not require sidekiq-limit_fetch' do + expect(described_class).not_to receive(:require).with('sidekiq-limit_fetch') - expect(Sidekiq::Queue['merge'].limit).to be_nil + described_class.execute! + end end end end diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb index 12cac1d033d..b47f3314926 100644 --- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb @@ -4,7 +4,7 @@ require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespacel describe CleanupNamespacelessPendingDeleteProjects do before do # Stub after_save callbacks that will fail when Project has no namespace - allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil) + allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil) end diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb index 260378adaa7..9b92f4b70b0 100644 --- a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb +++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb @@ -2,19 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170628080858_migrate_stage_id_reference_in_background') describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do - matcher :be_scheduled_migration do |delay, *expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] && - job['at'].to_i == (delay.to_i + Time.now.to_i) - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - let(:jobs) { table(:ci_builds) } let(:stages) { table(:ci_stages) } let(:pipelines) { table(:ci_pipelines) } diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb new file mode 100644 index 00000000000..4102d57e368 --- /dev/null +++ b/spec/migrations/migrate_stages_statuses_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb') + +describe MigrateStagesStatuses, :migration do + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + STATUSES = { created: 0, pending: 1, running: 2, success: 3, + failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + stub_const("#{described_class.name}::RANGE_SIZE", 2) + + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2') + + pipelines.create!(id: 1, project_id: 1, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 2, ref: 'feature', sha: '21a3deb') + + create_job(project: 1, pipeline: 1, stage: 'test', status: 'success') + create_job(project: 1, pipeline: 1, stage: 'test', status: 'running') + create_job(project: 1, pipeline: 1, stage: 'build', status: 'success') + create_job(project: 1, pipeline: 1, stage: 'build', status: 'failed') + create_job(project: 2, pipeline: 2, stage: 'test', status: 'success') + create_job(project: 2, pipeline: 2, stage: 'test', status: 'success') + create_job(project: 2, pipeline: 2, stage: 'test', status: 'failed', retried: true) + + stages.create!(id: 1, pipeline_id: 1, project_id: 1, name: 'test', status: nil) + stages.create!(id: 2, pipeline_id: 1, project_id: 1, name: 'build', status: nil) + stages.create!(id: 3, pipeline_id: 2, project_id: 2, name: 'test', status: nil) + end + + it 'correctly migrates stages statuses' do + Sidekiq::Testing.inline! do + expect(stages.where(status: nil).count).to eq 3 + + migrate! + + expect(stages.where(status: nil)).to be_empty + expect(stages.all.order('id ASC').pluck(:status)) + .to eq [STATUSES[:running], STATUSES[:failed], STATUSES[:success]] + end + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3) + expect(BackgroundMigrationWorker.jobs.size).to eq 2 + end + end + end + + def create_job(project:, pipeline:, stage:, status:, **opts) + stages = { test: 1, build: 2, deploy: 3 } + + jobs.create!(project_id: project, commit_id: pipeline, + stage_idx: stages[stage.to_sym], stage: stage, + status: status, **opts) + end +end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb new file mode 100644 index 00000000000..74c9d6145e2 --- /dev/null +++ b/spec/models/ci/stage_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Ci::Stage, :models do + let(:stage) { create(:ci_stage_entity) } + + describe 'associations' do + before do + create(:ci_build, stage_id: stage.id) + create(:commit_status, stage_id: stage.id) + end + + describe '#statuses' do + it 'returns all commit statuses' do + expect(stage.statuses.count).to be 2 + end + end + + describe '#builds' do + it 'returns only builds' do + expect(stage.builds).to be_one + end + end + end + + describe '#status' do + context 'when stage is pending' do + let(:stage) { create(:ci_stage_entity, status: 'pending') } + + it 'has a correct status value' do + expect(stage.status).to eq 'pending' + end + end + + context 'when stage is success' do + let(:stage) { create(:ci_stage_entity, status: 'success') } + + it 'has a correct status value' do + expect(stage.status).to eq 'success' + end + end + end + + describe 'update_status' do + context 'when stage objects needs to be updated' do + before do + create(:ci_build, :success, stage_id: stage.id) + create(:ci_build, :running, stage_id: stage.id) + end + + it 'updates stage status correctly' do + expect { stage.update_status } + .to change { stage.reload.status } + .to 'running' + end + end + + context 'when stage is skipped' do + it 'updates status to skipped' do + expect { stage.update_status } + .to change { stage.reload.status } + .to 'skipped' + end + end + + context 'when stage object is locked' do + before do + create(:ci_build, :failed, stage_id: stage.id) + end + + it 'retries a lock to update a stage status' do + stage.lock_version = 100 + + stage.update_status + + expect(stage.reload).to be_failed + end + end + end +end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 8c4a366ef8f..f7583645e69 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -7,10 +7,10 @@ describe CommitStatus do create(:ci_pipeline, project: project, sha: project.commit.id) end - let(:commit_status) { create_status } + let(:commit_status) { create_status(stage: 'test') } - def create_status(args = {}) - create(:commit_status, args.merge(pipeline: pipeline)) + def create_status(**opts) + create(:commit_status, pipeline: pipeline, **opts) end it { is_expected.to belong_to(:pipeline) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5e60511f3a8..2e613c44357 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1251,60 +1251,6 @@ describe Project do end end - describe '#rename_repo' do - let(:project) { create(:project, :repository) } - let(:gitlab_shell) { Gitlab::Shell.new } - - before do - # Project#gitlab_shell returns a new instance of Gitlab::Shell on every - # call. This makes testing a bit easier. - allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - end - - it 'renames a repository' do - stub_container_registry_config(enabled: false) - - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}") - .and_return(true) - - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") - .and_return(true) - - expect_any_instance_of(SystemHooksService) - .to receive(:execute_hooks_for) - .with(project, :rename) - - expect_any_instance_of(Gitlab::UploadsTransfer) - .to receive(:rename_project) - .with('foo', project.path, project.namespace.full_path) - - expect(project).to receive(:expire_caches_before_rename) - - expect(project).to receive(:expires_full_path_cache) - - project.rename_repo - end - - context 'container registry with images' do - let(:container_repository) { create(:container_repository) } - - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: :any, tags: ['tag']) - project.container_repositories << container_repository - end - - subject { project.rename_repo } - - it { expect {subject}.to raise_error(StandardError) } - end - end - describe '#expire_caches_before_rename' do let(:project) { create(:project, :repository) } let(:repo) { double(:repo, exists?: true) } @@ -2367,4 +2313,181 @@ describe Project do expect(project.forks_count).to eq(1) end end + + context 'legacy storage' do + let(:project) { create(:project, :repository) } + let(:gitlab_shell) { Gitlab::Shell.new } + + before do + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + end + + describe '#base_dir' do + it 'returns base_dir based on namespace only' do + expect(project.base_dir).to eq(project.namespace.full_path) + end + end + + describe '#disk_path' do + it 'returns disk_path based on namespace and project path' do + expect(project.disk_path).to eq("#{project.namespace.full_path}/#{project.path}") + end + end + + describe '#ensure_storage_path_exists' do + it 'delegates to gitlab_shell to ensure namespace is created' do + expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, project.base_dir) + + project.ensure_storage_path_exists + end + end + + describe '#legacy_storage?' do + it 'returns true when storage_version is nil' do + project = build(:project) + + expect(project.legacy_storage?).to be_truthy + end + end + + describe '#rename_repo' do + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + end + + it 'renames a repository' do + stub_container_registry_config(enabled: false) + + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}") + .and_return(true) + + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") + .and_return(true) + + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(project, :rename) + + expect_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:rename_project) + .with('foo', project.path, project.namespace.full_path) + + expect(project).to receive(:expire_caches_before_rename) + + expect(project).to receive(:expires_full_path_cache) + + project.rename_repo + end + + context 'container registry with images' do + let(:container_repository) { create(:container_repository) } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: :any, tags: ['tag']) + project.container_repositories << container_repository + end + + subject { project.rename_repo } + + it { expect { subject }.to raise_error(StandardError) } + end + end + + describe '#pages_path' do + it 'returns a path where pages are stored' do + expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) + end + end + end + + context 'hashed storage' do + let(:project) { create(:project, :repository) } + let(:gitlab_shell) { Gitlab::Shell.new } + let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } + + before do + stub_application_setting(hashed_storage_enabled: true) + allow(Digest::SHA2).to receive(:hexdigest) { hash } + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + end + + describe '#base_dir' do + it 'returns base_dir based on hash of project id' do + expect(project.base_dir).to eq('@hashed/6b/86') + end + end + + describe '#disk_path' do + it 'returns disk_path based on hash of project id' do + hashed_path = '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' + + expect(project.disk_path).to eq(hashed_path) + end + end + + describe '#ensure_storage_path_exists' do + it 'delegates to gitlab_shell to ensure namespace is created' do + expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, '@hashed/6b/86') + + project.ensure_storage_path_exists + end + end + + describe '#rename_repo' do + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + end + + it 'renames a repository' do + stub_container_registry_config(enabled: false) + + expect(gitlab_shell).not_to receive(:mv_repository) + + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(project, :rename) + + expect_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:rename_project) + .with('foo', project.path, project.namespace.full_path) + + expect(project).to receive(:expire_caches_before_rename) + + expect(project).to receive(:expires_full_path_cache) + + project.rename_repo + end + + context 'container registry with images' do + let(:container_repository) { create(:container_repository) } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: :any, tags: ['tag']) + project.container_repositories << container_repository + end + + subject { project.rename_repo } + + it { expect { subject }.to raise_error(StandardError) } + end + end + + describe '#pages_path' do + it 'returns a path where pages are stored' do + expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) + end + end + end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index b17a93e3fbe..cf420ae3ea6 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -123,6 +123,36 @@ describe GroupPolicy do end end + describe 'when nested group support feature is disabled' do + before do + allow(Group).to receive(:supports_nested_groups?).and_return(false) + end + + context 'admin' do + let(:current_user) { admin } + + it 'allows every owner permission except creating subgroups' do + create_subgroup_permission = [:create_subgroup] + updated_owner_permissions = owner_permissions - create_subgroup_permission + + expect_disallowed(*create_subgroup_permission) + expect_allowed(*updated_owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it 'allows every owner permission except creating subgroups' do + create_subgroup_permission = [:create_subgroup] + updated_owner_permissions = owner_permissions - create_subgroup_permission + + expect_disallowed(*create_subgroup_permission) + expect_allowed(*updated_owner_permissions) + end + end + end + describe 'private nested group use the highest access level from the group and inherited permissions', :nested_groups do let(:nested_group) { create(:group, :private, parent: group) } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb deleted file mode 100644 index 7ccba4ba3ec..00000000000 --- a/spec/requests/ci/api/builds_spec.rb +++ /dev/null @@ -1,912 +0,0 @@ -require 'spec_helper' - -describe Ci::API::Builds do - let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) } - let(:project) { FactoryGirl.create(:project, shared_runners_enabled: false) } - let(:last_update) { nil } - - describe "Builds API for runners" do - let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') } - - before do - project.runners << runner - end - - describe "POST /builds/register" do - let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' } - let!(:last_update) { } - let!(:new_update) { } - - before do - stub_container_registry_config(enabled: false) - end - - shared_examples 'no builds available' do - context 'when runner sends version in User-Agent' do - context 'for stable version' do - it 'gives 204 and set X-GitLab-Last-Update' do - expect(response).to have_http_status(204) - expect(response.header).to have_key('X-GitLab-Last-Update') - end - end - - context 'when last_update is up-to-date' do - let(:last_update) { runner.ensure_runner_queue_value } - - it 'gives 204 and set the same X-GitLab-Last-Update' do - expect(response).to have_http_status(204) - expect(response.header['X-GitLab-Last-Update']) - .to eq(last_update) - end - end - - context 'when last_update is outdated' do - let(:last_update) { runner.ensure_runner_queue_value } - let(:new_update) { runner.tick_runner_queue } - - it 'gives 204 and set a new X-GitLab-Last-Update' do - expect(response).to have_http_status(204) - expect(response.header['X-GitLab-Last-Update']) - .to eq(new_update) - end - end - - context 'for beta version' do - let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (1-5-stable; go1.6.3; linux/amd64)' } - it { expect(response).to have_http_status(204) } - end - end - - context "when runner doesn't send version in User-Agent" do - let(:user_agent) { 'Go-http-client/1.1' } - it { expect(response).to have_http_status(404) } - end - - context "when runner doesn't have a User-Agent" do - let(:user_agent) { nil } - it { expect(response).to have_http_status(404) } - end - end - - context 'when an old image syntax is used' do - before do - build.update!(options: { image: 'codeclimate' }) - end - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "image" => "codeclimate" }) - end - end - - context 'when a new image syntax is used' do - before do - build.update!(options: { image: { name: 'codeclimate' } }) - end - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "image" => "codeclimate" }) - end - end - - context 'when an old service syntax is used' do - before do - build.update!(options: { services: ['mysql'] }) - end - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "services" => ["mysql"] }) - end - end - - context 'when a new service syntax is used' do - before do - build.update!(options: { services: [name: 'mysql'] }) - end - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "services" => ["mysql"] }) - end - end - - context 'when no image or service is defined' do - before do - build.update!(options: {}) - end - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - - expect(json_response["options"]).to be_empty - end - end - - context 'when there is a pending build' do - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(response.headers).not_to have_key('X-GitLab-Last-Update') - expect(json_response['sha']).to eq(build.sha) - expect(runner.reload.platform).to eq("darwin") - expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) - expect(json_response["variables"]).to include( - { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true } - ) - end - - it 'updates runner info' do - expect { register_builds }.to change { runner.reload.contacted_at } - end - - context 'when concurrently updating build' do - before do - expect_any_instance_of(Ci::Build).to receive(:run!) - .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) - end - - it 'returns a conflict' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(409) - expect(response.headers).not_to have_key('X-GitLab-Last-Update') - end - end - - context 'registry credentials' do - let(:registry_credentials) do - { 'type' => 'registry', - 'url' => 'registry.example.com:5005', - 'username' => 'gitlab-ci-token', - 'password' => build.token } - end - - context 'when registry is enabled' do - before do - stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005') - end - - it 'sends registry credentials key' do - register_builds info: { platform: :darwin } - - expect(json_response).to have_key('credentials') - expect(json_response['credentials']).to include(registry_credentials) - end - end - - context 'when registry is disabled' do - before do - stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005') - end - - it 'does not send registry credentials' do - register_builds info: { platform: :darwin } - - expect(json_response).to have_key('credentials') - expect(json_response['credentials']).not_to include(registry_credentials) - end - end - end - - context 'when docker configuration options are used' do - let!(:build) { create(:ci_build, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - - it 'starts a build' do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response['options']['image']).to eq('ruby:2.1') - expect(json_response['options']['services']).to eq(['postgres', 'docker:dind']) - end - end - end - - context 'when builds are finished' do - before do - build.success - register_builds - end - - it_behaves_like 'no builds available' - end - - context 'for other project with builds' do - before do - build.success - create(:ci_build, :pending) - register_builds - end - - it_behaves_like 'no builds available' - end - - context 'for shared runner' do - let!(:runner) { create(:ci_runner, :shared, token: "SharedRunner") } - - before do - register_builds(runner.token) - end - - it_behaves_like 'no builds available' - end - - context 'for triggered build' do - before do - trigger = create(:ci_trigger, project: project) - create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") - end - - it "returns variables for triggers" do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["variables"]).to include( - { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true }, - { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } - ) - end - end - - context 'with multiple builds' do - before do - build.success - end - - let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - - it "returns dependent builds" do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["id"]).to eq(test_build.id) - expect(json_response["depends_on_builds"].count).to eq(1) - expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach') - end - end - - %w(name version revision platform architecture).each do |param| - context "updates runner #{param}" do - let(:value) { "#{param}_value" } - - subject { runner.read_attribute(param.to_sym) } - - it do - register_builds info: { param => value } - - expect(response).to have_http_status(201) - runner.reload - is_expected.to eq(value) - end - end - end - - context 'when build has no tags' do - before do - build.update(tags: []) - end - - context 'when runner is allowed to pick untagged builds' do - before do - runner.update_column(:run_untagged, true) - end - - it 'picks build' do - register_builds - - expect(response).to have_http_status 201 - end - end - - context 'when runner is not allowed to pick untagged builds' do - before do - runner.update_column(:run_untagged, false) - register_builds - end - - it_behaves_like 'no builds available' - end - end - - context 'when runner is paused' do - let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') } - - it 'responds with 404' do - register_builds - - expect(response).to have_http_status 404 - end - - it 'does not update runner info' do - expect { register_builds } - .not_to change { runner.reload.contacted_at } - end - end - - def register_builds(token = runner.token, **params) - new_params = params.merge(token: token, last_update: last_update) - - post ci_api("/builds/register"), new_params, { 'User-Agent' => user_agent } - end - end - - describe "PUT /builds/:id" do - let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } - - before do - build.run! - put ci_api("/builds/#{build.id}"), token: runner.token - end - - it "updates a running build" do - expect(response).to have_http_status(200) - end - - it 'does not override trace information when no trace is given' do - expect(build.reload.trace.raw).to eq 'BUILD TRACE' - end - - context 'job has been erased' do - let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } - - it 'responds with forbidden' do - expect(response.status).to eq 403 - end - end - end - - describe 'PATCH /builds/:id/trace.txt' do - let(:build) do - attributes = { runner_id: runner.id, pipeline: pipeline } - create(:ci_build, :running, :trace, attributes) - end - - let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } - let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } - let(:update_interval) { 10.seconds.to_i } - - def patch_the_trace(content = ' appended', request_headers = nil) - unless request_headers - build.trace.read do |stream| - offset = stream.size - limit = offset + content.length - 1 - request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" }) - end - end - - Timecop.travel(build.updated_at + update_interval) do - patch ci_api("/builds/#{build.id}/trace.txt"), content, request_headers - build.reload - end - end - - def initial_patch_the_trace - patch_the_trace(' appended', headers_with_range) - end - - def force_patch_the_trace - 2.times { patch_the_trace('') } - end - - before do - initial_patch_the_trace - end - - context 'when request is valid' do - it 'gets correct response' do - expect(response.status).to eq 202 - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended' - expect(response.header).to have_key 'Range' - expect(response.header).to have_key 'Build-Status' - end - - context 'when build has been updated recently' do - it { expect { patch_the_trace }.not_to change { build.updated_at }} - - it 'changes the build trace' do - patch_the_trace - - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended appended' - end - - context 'when Runner makes a force-patch' do - it { expect { force_patch_the_trace }.not_to change { build.updated_at }} - - it "doesn't change the build.trace" do - force_patch_the_trace - - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended' - end - end - end - - context 'when build was not updated recently' do - let(:update_interval) { 15.minutes.to_i } - - it { expect { patch_the_trace }.to change { build.updated_at } } - - it 'changes the build.trace' do - patch_the_trace - - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended appended' - end - - context 'when Runner makes a force-patch' do - it { expect { force_patch_the_trace }.to change { build.updated_at } } - - it "doesn't change the build.trace" do - force_patch_the_trace - - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended' - end - end - end - - context 'when project for the build has been deleted' do - let(:build) do - attributes = { runner_id: runner.id, pipeline: pipeline } - create(:ci_build, :running, :trace, attributes) do |build| - build.project.update(pending_delete: true) - end - end - - it 'responds with forbidden' do - expect(response.status).to eq(403) - end - end - end - - context 'when Runner makes a force-patch' do - before do - force_patch_the_trace - end - - it 'gets correct response' do - expect(response.status).to eq 202 - expect(build.reload.trace.raw).to eq 'BUILD TRACE appended' - expect(response.header).to have_key 'Range' - expect(response.header).to have_key 'Build-Status' - end - end - - context 'when content-range start is too big' do - let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) } - - it 'gets 416 error response with range headers' do - expect(response.status).to eq 416 - expect(response.header).to have_key 'Range' - expect(response.header['Range']).to eq '0-11' - end - end - - context 'when content-range start is too small' do - let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) } - - it 'gets 416 error response with range headers' do - expect(response.status).to eq 416 - expect(response.header).to have_key 'Range' - expect(response.header['Range']).to eq '0-11' - end - end - - context 'when Content-Range header is missing' do - let(:headers_with_range) { headers } - - it { expect(response.status).to eq 400 } - end - - context 'when build has been errased' do - let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } - - it { expect(response.status).to eq 403 } - end - end - - context "Artifacts" do - let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) } - let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } - let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } - let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } - let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } - let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } - let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } - let(:token) { build.token } - let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) } - - before do - build.run! - end - - describe "POST /builds/:id/artifacts/authorize" do - context "authorizes posting artifact to running build" do - it "using token as parameter" do - post authorize_url, { token: build.token }, headers - - expect(response).to have_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response["TempPath"]).not_to be_nil - end - - it "using token as header" do - post authorize_url, {}, headers_with_token - - expect(response).to have_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response["TempPath"]).not_to be_nil - end - - it "using runners token" do - post authorize_url, { token: build.project.runners_token }, headers - - expect(response).to have_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response["TempPath"]).not_to be_nil - end - - it "reject requests that did not go through gitlab-workhorse" do - headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) - - post authorize_url, { token: build.token }, headers - - expect(response).to have_http_status(500) - end - end - - context "fails to post too large artifact" do - it "using token as parameter" do - stub_application_setting(max_artifacts_size: 0) - - post authorize_url, { token: build.token, filesize: 100 }, headers - - expect(response).to have_http_status(413) - end - - it "using token as header" do - stub_application_setting(max_artifacts_size: 0) - - post authorize_url, { filesize: 100 }, headers_with_token - - expect(response).to have_http_status(413) - end - end - - context 'authorization token is invalid' do - before do - post authorize_url, { token: 'invalid', filesize: 100 } - end - - it 'responds with forbidden' do - expect(response).to have_http_status(403) - end - end - end - - describe "POST /builds/:id/artifacts" do - context "disable sanitizer" do - before do - # by configuring this path we allow to pass temp file from any path - allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') - end - - describe 'build has been erased' do - let(:build) { create(:ci_build, erased_at: Time.now) } - - before do - upload_artifacts(file_upload, headers_with_token) - end - - it 'responds with forbidden' do - expect(response.status).to eq 403 - end - end - - describe 'uploading artifacts for a running build' do - shared_examples 'successful artifacts upload' do - it 'updates successfully' do - response_filename = - json_response['artifacts_file']['filename'] - - expect(response).to have_http_status(201) - expect(response_filename).to eq(file_upload.original_filename) - end - end - - context 'uses regular file post' do - before do - upload_artifacts(file_upload, headers_with_token, false) - end - - it_behaves_like 'successful artifacts upload' - end - - context 'uses accelerated file post' do - before do - upload_artifacts(file_upload, headers_with_token, true) - end - - it_behaves_like 'successful artifacts upload' - end - - context 'updates artifact' do - before do - upload_artifacts(file_upload2, headers_with_token) - upload_artifacts(file_upload, headers_with_token) - end - - it_behaves_like 'successful artifacts upload' - end - - context 'when using runners token' do - let(:token) { build.project.runners_token } - - before do - upload_artifacts(file_upload, headers_with_token) - end - - it_behaves_like 'successful artifacts upload' - end - end - - context 'posts artifacts file and metadata file' do - let!(:artifacts) { file_upload } - let!(:metadata) { file_upload2 } - - let(:stored_artifacts_file) { build.reload.artifacts_file.file } - let(:stored_metadata_file) { build.reload.artifacts_metadata.file } - let(:stored_artifacts_size) { build.reload.artifacts_size } - - before do - post(post_url, post_data, headers_with_token) - end - - context 'posts data accelerated by workhorse is correct' do - let(:post_data) do - { 'file.path' => artifacts.path, - 'file.name' => artifacts.original_filename, - 'metadata.path' => metadata.path, - 'metadata.name' => metadata.original_filename } - end - - it 'stores artifacts and artifacts metadata' do - expect(response).to have_http_status(201) - expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) - expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) - expect(stored_artifacts_size).to eq(71759) - end - end - - context 'no artifacts file in post data' do - let(:post_data) do - { 'metadata' => metadata } - end - - it 'is expected to respond with bad request' do - expect(response).to have_http_status(400) - end - - it 'does not store metadata' do - expect(stored_metadata_file).to be_nil - end - end - end - - context 'with an expire date' do - let!(:artifacts) { file_upload } - let(:default_artifacts_expire_in) {} - - let(:post_data) do - { 'file.path' => artifacts.path, - 'file.name' => artifacts.original_filename, - 'expire_in' => expire_in } - end - - before do - stub_application_setting( - default_artifacts_expire_in: default_artifacts_expire_in) - - post(post_url, post_data, headers_with_token) - end - - context 'with an expire_in given' do - let(:expire_in) { '7 days' } - - it 'updates when specified' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at) - .to be_within(5.minutes).of(7.days.from_now) - end - end - - context 'with no expire_in given' do - let(:expire_in) { nil } - - it 'ignores if not specified' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']).to be_nil - expect(build.artifacts_expire_at).to be_nil - end - - context 'with application default' do - context 'default to 5 days' do - let(:default_artifacts_expire_in) { '5 days' } - - it 'sets to application default' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']) - .not_to be_empty - expect(build.artifacts_expire_at) - .to be_within(5.minutes).of(5.days.from_now) - end - end - - context 'default to 0' do - let(:default_artifacts_expire_in) { '0' } - - it 'does not set expire_in' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']).to be_nil - expect(build.artifacts_expire_at).to be_nil - end - end - end - end - end - - context "artifacts file is too large" do - it "fails to post too large artifact" do - stub_application_setting(max_artifacts_size: 0) - upload_artifacts(file_upload, headers_with_token) - expect(response).to have_http_status(413) - end - end - - context "artifacts post request does not contain file" do - it "fails to post artifacts without file" do - post post_url, {}, headers_with_token - expect(response).to have_http_status(400) - end - end - - context 'GitLab Workhorse is not configured' do - it "fails to post artifacts without GitLab-Workhorse" do - post post_url, { token: build.token }, {} - expect(response).to have_http_status(403) - end - end - end - - context "artifacts are being stored outside of tmp path" do - before do - # by configuring this path we allow to pass file from @tmpdir only - # but all temporary files are stored in system tmp directory - @tmpdir = Dir.mktmpdir - allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) - end - - after do - FileUtils.remove_entry @tmpdir - end - - it "fails to post artifacts for outside of tmp path" do - upload_artifacts(file_upload, headers_with_token) - expect(response).to have_http_status(400) - end - end - - def upload_artifacts(file, headers = {}, accelerated = true) - if accelerated - post post_url, { - 'file.path' => file.path, - 'file.name' => file.original_filename - }, headers - else - post post_url, { file: file }, headers - end - end - end - - describe 'DELETE /builds/:id/artifacts' do - let(:build) { create(:ci_build, :artifacts) } - - before do - delete delete_url, token: build.token - end - - shared_examples 'having removable artifacts' do - it 'removes build artifacts' do - build.reload - - expect(response).to have_http_status(200) - expect(build.artifacts_file.exists?).to be_falsy - expect(build.artifacts_metadata.exists?).to be_falsy - expect(build.artifacts_size).to be_nil - end - end - - context 'when using build token' do - before do - delete delete_url, token: build.token - end - - it_behaves_like 'having removable artifacts' - end - - context 'when using runnners token' do - before do - delete delete_url, token: build.project.runners_token - end - - it_behaves_like 'having removable artifacts' - end - end - - describe 'GET /builds/:id/artifacts' do - before do - get get_url, token: token - end - - context 'build has artifacts' do - let(:build) { create(:ci_build, :artifacts) } - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } - end - - shared_examples 'having downloadable artifacts' do - it 'download artifacts' do - expect(response).to have_http_status(200) - expect(response.headers).to include download_headers - end - end - - context 'when using build token' do - let(:token) { build.token } - - it_behaves_like 'having downloadable artifacts' - end - - context 'when using runnners token' do - let(:token) { build.project.runners_token } - - it_behaves_like 'having downloadable artifacts' - end - end - - context 'build does not has artifacts' do - let(:token) { build.token } - - it 'responds with not found' do - expect(response).to have_http_status(404) - end - end - end - end - end -end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb deleted file mode 100644 index 75059dd20a0..00000000000 --- a/spec/requests/ci/api/runners_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -require 'spec_helper' - -describe Ci::API::Runners do - include StubGitlabCalls - - let(:registration_token) { 'abcdefg123456' } - - before do - stub_gitlab_calls - stub_application_setting(runners_registration_token: registration_token) - end - - describe "POST /runners/register" do - context 'when runner token is provided' do - before do - post ci_api("/runners/register"), token: registration_token - end - - it 'creates runner with default values' do - expect(response).to have_http_status 201 - expect(Ci::Runner.first.run_untagged).to be true - expect(Ci::Runner.first.token).not_to eq(registration_token) - end - end - - context 'when runner description is provided' do - before do - post ci_api("/runners/register"), token: registration_token, - description: "server.hostname" - end - - it 'creates runner' do - expect(response).to have_http_status 201 - expect(Ci::Runner.first.description).to eq("server.hostname") - end - end - - context 'when runner tags are provided' do - before do - post ci_api("/runners/register"), token: registration_token, - tag_list: "tag1, tag2" - end - - it 'creates runner' do - expect(response).to have_http_status 201 - expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) - end - end - - context 'when option for running untagged jobs is provided' do - context 'when tags are provided' do - it 'creates runner' do - post ci_api("/runners/register"), token: registration_token, - run_untagged: false, - tag_list: ['tag'] - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.run_untagged).to be false - end - end - - context 'when tags are not provided' do - it 'does not create runner' do - post ci_api("/runners/register"), token: registration_token, - run_untagged: false - - expect(response).to have_http_status 404 - end - end - end - - context 'when project token is provided' do - let(:project) { FactoryGirl.create(:project) } - - before do - post ci_api("/runners/register"), token: project.runners_token - end - - it 'creates runner' do - expect(response).to have_http_status 201 - expect(project.runners.size).to eq(1) - expect(Ci::Runner.first.token).not_to eq(registration_token) - expect(Ci::Runner.first.token).not_to eq(project.runners_token) - end - end - - context 'when token is invalid' do - it 'returns 403 error' do - post ci_api("/runners/register"), token: 'invalid' - - expect(response).to have_http_status 403 - end - end - - context 'when no token provided' do - it 'returns 400 error' do - post ci_api("/runners/register") - - expect(response).to have_http_status 400 - end - end - - %w(name version revision platform architecture).each do |param| - context "creates runner with #{param} saved" do - let(:value) { "#{param}_value" } - - subject { Ci::Runner.first.read_attribute(param.to_sym) } - - it do - post ci_api("/runners/register"), token: registration_token, info: { param => value } - expect(response).to have_http_status 201 - is_expected.to eq(value) - end - end - end - end - - describe "DELETE /runners/delete" do - it 'returns 200' do - runner = FactoryGirl.create(:ci_runner) - delete ci_api("/runners/delete"), token: runner.token - - expect(response).to have_http_status 200 - expect(Ci::Runner.count).to eq(0) - end - end -end diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb deleted file mode 100644 index 7c77ebb69a2..00000000000 --- a/spec/requests/ci/api/triggers_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'spec_helper' - -describe Ci::API::Triggers do - describe 'POST /projects/:project_id/refs/:ref/trigger' do - let!(:trigger_token) { 'secure token' } - let!(:project) { create(:project, :repository, ci_id: 10) } - let!(:project2) { create(:project, ci_id: 11) } - - let!(:trigger) do - create(:ci_trigger, - project: project, - token: trigger_token, - owner: create(:user)) - end - - let(:options) do - { - token: trigger_token - } - end - - before do - stub_ci_pipeline_to_return_yaml_file - - project.add_developer(trigger.owner) - end - - context 'Handles errors' do - it 'returns bad request if token is missing' do - post ci_api("/projects/#{project.ci_id}/refs/master/trigger") - expect(response).to have_http_status(400) - end - - it 'returns not found if project is not found' do - post ci_api('/projects/0/refs/master/trigger'), options - expect(response).to have_http_status(404) - end - - it 'returns unauthorized if token is for different project' do - post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options - expect(response).to have_http_status(401) - end - end - - context 'Have a commit' do - let(:pipeline) { project.pipelines.last } - - it 'creates builds' do - post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options - expect(response).to have_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.pending.size).to eq(2) - expect(pipeline.builds.size).to eq(5) - end - - it 'returns bad request with no builds created if there\'s no commit for that ref' do - post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options - expect(response).to have_http_status(400) - expect(json_response['message']['base']) - .to contain_exactly('Reference not found') - end - - context 'Validates variables' do - let(:variables) do - { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } - end - - it 'validates variables to be a hash' do - post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value') - expect(response).to have_http_status(400) - - expect(json_response['error']).to eq('variables is invalid') - end - - it 'validates variables needs to be a map of key-valued strings' do - post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) - expect(response).to have_http_status(400) - expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') - end - - it 'creates trigger request with variables' do - post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables) - expect(response).to have_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.first.trigger_request.variables).to eq(variables) - end - end - end - end -end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 53d4fcfed18..8465a6f99bd 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -55,10 +55,15 @@ describe Ci::CreatePipelineService do context 'when merge requests already exist for this source branch' do it 'updates head pipeline of each merge request' do - merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) - merge_request_2 = create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project) + merge_request_1 = create(:merge_request, source_branch: 'master', + target_branch: "branch_1", + source_project: project) - head_pipeline = pipeline + merge_request_2 = create(:merge_request, source_branch: 'master', + target_branch: "branch_2", + source_project: project) + + head_pipeline = execute_service expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline) expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline) @@ -66,9 +71,11 @@ describe Ci::CreatePipelineService do context 'when there is no pipeline for source branch' do it "does not update merge request head pipeline" do - merge_request = create(:merge_request, source_branch: 'feature', target_branch: "branch_1", source_project: project) + merge_request = create(:merge_request, source_branch: 'feature', + target_branch: "branch_1", + source_project: project) - head_pipeline = pipeline + head_pipeline = execute_service expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline) end @@ -76,13 +83,19 @@ describe Ci::CreatePipelineService do context 'when merge request target project is different from source project' do let!(:target_project) { create(:project, :repository) } - let!(:forked_project_link) { create(:forked_project_link, forked_to_project: project, forked_from_project: target_project) } + + let!(:forked_project_link) do + create(:forked_project_link, forked_to_project: project, + forked_from_project: target_project) + end it 'updates head pipeline for merge request' do - merge_request = - create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project, target_project: target_project) + merge_request = create(:merge_request, source_branch: 'master', + target_branch: "branch_1", + source_project: project, + target_project: target_project) - head_pipeline = pipeline + head_pipeline = execute_service expect(merge_request.reload.head_pipeline).to eq(head_pipeline) end @@ -90,15 +103,36 @@ describe Ci::CreatePipelineService do context 'when the pipeline is not the latest for the branch' do it 'does not update merge request head pipeline' do - merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) + merge_request = create(:merge_request, source_branch: 'master', + target_branch: "branch_1", + source_project: project) - allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false) + allow_any_instance_of(Ci::Pipeline) + .to receive(:latest?).and_return(false) - pipeline + execute_service expect(merge_request.reload.head_pipeline).to be_nil end end + + context 'when pipeline has errors' do + before do + stub_ci_pipeline_yaml_file('some invalid syntax') + end + + it 'updates merge request head pipeline reference' do + merge_request = create(:merge_request, source_branch: 'master', + target_branch: 'feature', + source_project: project) + + head_pipeline = execute_service + + expect(head_pipeline).to be_persisted + expect(head_pipeline.yaml_errors).to be_present + expect(merge_request.reload.head_pipeline).to eq head_pipeline + end + end end context 'auto-cancel enabled' do diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index b2175717a70..6973e7ff990 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -32,12 +32,24 @@ describe Groups::CreateService, '#execute' do end it { is_expected.to be_persisted } + + context 'when nested groups feature is disabled' do + it 'does not save group and returns an error' do + allow(Group).to receive(:supports_nested_groups?).and_return(false) + + is_expected.not_to be_persisted + expect(subject.errors[:parent_id]).to include('You don’t have permission to create a subgroup in this group.') + expect(subject.parent_id).to be_nil + end + end end context 'as guest' do it 'does not save group and returns an error' do + allow(Group).to receive(:supports_nested_groups?).and_return(true) + is_expected.not_to be_persisted - expect(subject.errors[:parent_id].first).to eq('manage access required to create subgroup') + expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.') expect(subject.parent_id).to be_nil end end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 1b2ce3cd03e..ac4b9c02ba7 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -8,8 +8,8 @@ describe Groups::DestroyService do let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:project, namespace: group) } let!(:notification_setting) { create(:notification_setting, source: group)} - let!(:gitlab_shell) { Gitlab::Shell.new } - let!(:remove_path) { group.path + "+#{group.id}+deleted" } + let(:gitlab_shell) { Gitlab::Shell.new } + let(:remove_path) { group.path + "+#{group.id}+deleted" } before do group.add_user(user, Gitlab::Access::OWNER) @@ -134,4 +134,26 @@ describe Groups::DestroyService do it_behaves_like 'group destruction', false end + + describe 'repository removal' do + before do + destroy_group(group, user, false) + end + + context 'legacy storage' do + let!(:project) { create(:project, :empty_repo, namespace: group) } + + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey + end + end + + context 'hashed storage' do + let!(:project) { create(:project, :hashed, :empty_repo, namespace: group) } + + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey + end + end + end end diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 492b55cdece..313f87ae1f6 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -2,8 +2,10 @@ require 'spec_helper' describe MergeRequests::CreateFromIssueService do let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + let(:label_ids) { create_pair(:label, project: project).map(&:id) } + let(:milestone_id) { create(:milestone, project: project).id } + let(:issue) { create(:issue, project: project, milestone_id: milestone_id) } subject(:service) { described_class.new(project, user, issue_iid: issue.iid) } @@ -25,6 +27,20 @@ describe MergeRequests::CreateFromIssueService do described_class.new(project, user, issue_iid: -1).execute end + it "inherits labels" do + issue.assign_attributes(label_ids: label_ids) + + result = service.execute + + expect(result[:merge_request].label_ids).to eq(label_ids) + end + + it "inherits milestones" do + result = service.execute + + expect(result[:merge_request].milestone_id).to eq(milestone_id) + end + it 'delegates the branch creation to CreateBranchService' do expect_any_instance_of(CreateBranchService).to receive(:execute).once.and_call_original diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index a82567f6f43..58a5bede3de 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -4,9 +4,10 @@ describe Users::DestroyService do describe "Deletes a user and all their personal projects" do let!(:user) { create(:user) } let!(:admin) { create(:admin) } - let!(:namespace) { create(:namespace, owner: user) } + let!(:namespace) { user.namespace } let!(:project) { create(:project, namespace: namespace) } let(:service) { described_class.new(admin) } + let(:gitlab_shell) { Gitlab::Shell.new } context 'no options are given' do it 'deletes the user' do @@ -14,7 +15,7 @@ describe Users::DestroyService do expect { user_data['email'].to eq(user.email) } expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) - expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { Namespace.with_deleted.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'will delete the project' do @@ -165,5 +166,27 @@ describe Users::DestroyService do expect(Issue.exists?(issue.id)).to be_falsy end end + + describe "user personal's repository removal" do + before do + Sidekiq::Testing.inline! { service.execute(user) } + end + + context 'legacy storage' do + let!(:project) { create(:project, :empty_repo, namespace: user.namespace) } + + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey + end + end + + context 'hashed storage' do + let!(:project) { create(:project, :empty_repo, :hashed, namespace: user.namespace) } + + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey + end + end + end end end diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/background_migrations_matchers.rb new file mode 100644 index 00000000000..423c0e4cefc --- /dev/null +++ b/spec/support/background_migrations_matchers.rb @@ -0,0 +1,13 @@ +RSpec::Matchers.define :be_scheduled_migration do |delay, *expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration, expected] && + job['at'].to_i == (delay.to_i + Time.now.to_i) + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` " \ + 'not scheduled in expected time!' + end +end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 03b9b99e263..f8385ae7c72 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -29,21 +29,27 @@ describe AuthorizedProjectsWorker do end describe '#perform' do - subject { described_class.new } + let(:user) { create(:user) } - it "refreshes user's authorized projects" do - user = create(:user) + subject(:job) { described_class.new } + it "refreshes user's authorized projects" do expect_any_instance_of(User).to receive(:refresh_authorized_projects) - subject.perform(user.id) + job.perform(user.id) + end + + it 'notifies the JobWaiter when done if the key is provided' do + expect(Gitlab::JobWaiter).to receive(:notify).with('notify-key', job.jid) + + job.perform(user.id, 'notify-key') end context "when the user is not found" do it "does nothing" do expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) - subject.perform(-1) + job.perform(-1) end end end diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb index f2706254284..817e103fd9a 100644 --- a/spec/workers/namespaceless_project_destroy_worker_spec.rb +++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb @@ -5,7 +5,7 @@ describe NamespacelessProjectDestroyWorker do before do # Stub after_save callbacks that will fail when Project has no namespace - allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil) + allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil) end diff --git a/spec/workers/stage_update_worker_spec.rb b/spec/workers/stage_update_worker_spec.rb new file mode 100644 index 00000000000..7bc76c79464 --- /dev/null +++ b/spec/workers/stage_update_worker_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe StageUpdateWorker do + describe '#perform' do + context 'when stage exists' do + let(:stage) { create(:ci_stage_entity) } + + it 'updates stage status' do + expect_any_instance_of(Ci::Stage).to receive(:update_status) + + described_class.new.perform(stage.id) + end + end + + context 'when stage does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end |