summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-19 18:09:10 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-19 18:09:10 +0000
commit33795139ea8e72756bee3675b4e16387425e6ab1 (patch)
tree3ca568fca61482e57810ee30ad5ce4b964a82c4e /spec
parentc7e385e282bcb8505589bce526e692b7bb819ffa (diff)
downloadgitlab-ce-33795139ea8e72756bee3675b4e16387425e6ab1.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb4
-rw-r--r--spec/factories/resource_milestone_event.rb12
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb56
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_spec.rb9
-rw-r--r--spec/models/milestone_note_spec.rb18
-rw-r--r--spec/models/resource_milestone_event_spec.rb55
-rw-r--r--spec/rubocop/cop/migration/schedule_async_spec.rb152
-rw-r--r--spec/rubocop/cop/scalability/idempotent_worker_spec.rb38
-rw-r--r--spec/rubocop/migration_helpers_spec.rb56
-rw-r--r--spec/services/issuable/clone/attributes_rewriter_spec.rb42
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb44
-rw-r--r--spec/services/issues/update_service_spec.rb8
-rw-r--r--spec/services/merge_requests/update_service_spec.rb8
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb67
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/helpers/idempotent_worker_helper.rb15
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/resource_events.rb104
-rw-r--r--spec/support/shared_examples/workers/idempotency_shared_examples.rb31
-rw-r--r--spec/workers/background_migration_worker_spec.rb10
-rw-r--r--spec/workers/expire_job_cache_worker_spec.rb30
22 files changed, 744 insertions, 20 deletions
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 6b698c6da66..ee61ef73b45 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -135,6 +135,10 @@ describe Projects::MilestonesController do
end
describe "#destroy" do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
+ end
+
it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id)
diff --git a/spec/factories/resource_milestone_event.rb b/spec/factories/resource_milestone_event.rb
new file mode 100644
index 00000000000..86c54f2be68
--- /dev/null
+++ b/spec/factories/resource_milestone_event.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :resource_milestone_event do
+ issue { merge_request.nil? ? create(:issue) : nil }
+ merge_request { nil }
+ milestone
+ action { :add }
+ state { :opened }
+ user { issue&.author || merge_request&.author || create(:user) }
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4cf2553b90d..bb5475130cf 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1893,4 +1893,60 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ describe '#migrate_async' do
+ it 'calls BackgroundMigrationWorker.perform_async' do
+ expect(BackgroundMigrationWorker).to receive(:perform_async).with("Class", "hello", "world")
+
+ model.migrate_async("Class", "hello", "world")
+ end
+
+ it 'pushes a context with the current class name as caller_id' do
+ expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+
+ model.migrate_async('Class', 'hello', 'world')
+ end
+ end
+
+ describe '#migrate_in' do
+ it 'calls BackgroundMigrationWorker.perform_in' do
+ expect(BackgroundMigrationWorker).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World')
+
+ model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
+ end
+
+ it 'pushes a context with the current class name as caller_id' do
+ expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+
+ model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
+ end
+ end
+
+ describe '#bulk_migrate_async' do
+ it 'calls BackgroundMigrationWorker.bulk_perform_async' do
+ expect(BackgroundMigrationWorker).to receive(:bulk_perform_async).with([%w(Class hello world)])
+
+ model.bulk_migrate_async([%w(Class hello world)])
+ end
+
+ it 'pushes a context with the current class name as caller_id' do
+ expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+
+ model.bulk_migrate_async([%w(Class hello world)])
+ end
+ end
+
+ describe '#bulk_migrate_in' do
+ it 'calls BackgroundMigrationWorker.bulk_perform_in_' do
+ expect(BackgroundMigrationWorker).to receive(:bulk_perform_in).with(10.minutes, [%w(Class hello world)])
+
+ model.bulk_migrate_in(10.minutes, [%w(Class hello world)])
+ end
+
+ it 'pushes a context with the current class name as caller_id' do
+ expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+
+ model.bulk_migrate_in(10.minutes, [%w(Class hello world)])
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 4dadb310029..6c9a4bbfc71 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -9,6 +9,7 @@ issues:
- notes
- resource_label_events
- resource_weight_events
+- resource_milestone_events
- sentry_issue
- label_links
- labels
@@ -109,6 +110,7 @@ merge_requests:
- milestone
- notes
- resource_label_events
+- resource_milestone_events
- label_links
- labels
- last_edited_by
diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
index 38edd0f5eeb..fcdce8f9432 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::SidekiqConfig::Worker do
get_weight: attributes[:weight],
get_worker_resource_boundary: attributes[:resource_boundary],
latency_sensitive_worker?: attributes[:latency_sensitive],
- worker_has_external_dependencies?: attributes[:has_external_dependencies]
+ worker_has_external_dependencies?: attributes[:has_external_dependencies],
+ idempotent?: attributes[:idempotent]
)
described_class.new(inner_worker, ee: false)
@@ -89,7 +90,8 @@ describe Gitlab::SidekiqConfig::Worker do
has_external_dependencies: false,
latency_sensitive: false,
resource_boundary: :memory,
- weight: 2
+ weight: 2,
+ idempotent: true
}
attributes_b = {
@@ -97,7 +99,8 @@ describe Gitlab::SidekiqConfig::Worker do
has_external_dependencies: true,
latency_sensitive: true,
resource_boundary: :unknown,
- weight: 1
+ weight: 3,
+ idempotent: false
}
worker_a = create_worker(queue: 'a', **attributes_a)
diff --git a/spec/models/milestone_note_spec.rb b/spec/models/milestone_note_spec.rb
new file mode 100644
index 00000000000..9e77ef91bb2
--- /dev/null
+++ b/spec/models/milestone_note_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MilestoneNote do
+ describe '.from_event' do
+ let(:author) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { create(:issue, author: author, project: project) }
+ let(:event) { create(:resource_milestone_event, issue: noteable) }
+
+ subject { described_class.from_event(event, resource: noteable, resource_parent: project) }
+
+ it_behaves_like 'a system note', exclude_project: true do
+ let(:action) { 'milestone' }
+ end
+ end
+end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
new file mode 100644
index 00000000000..1b0181e3fd2
--- /dev/null
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceMilestoneEvent, type: :model do
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+
+ it_behaves_like 'having unique enum values'
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:milestone) }
+ end
+
+ describe 'validations' do
+ context 'when issue and merge_request are both nil' do
+ subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when issue and merge_request are both set' do
+ subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when issue is set' do
+ subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when merge_request is set' do
+ subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ describe 'states' do
+ [Issue, MergeRequest].each do |klass|
+ klass.available_states.each do |state|
+ it "supports state #{state.first} for #{klass.name.underscore}" do
+ model = create(klass.name.underscore, state: state[0])
+ key = model.class.name.underscore
+ event = build(described_class.name.underscore.to_sym, key => model, state: model.state)
+
+ expect(event.state).to eq(state[0])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/schedule_async_spec.rb b/spec/rubocop/cop/migration/schedule_async_spec.rb
new file mode 100644
index 00000000000..3453f1c51cc
--- /dev/null
+++ b/spec/rubocop/cop/migration/schedule_async_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/schedule_async'
+
+describe RuboCop::Cop::Migration::ScheduleAsync do
+ include CopHelper
+
+ let(:cop) { described_class.new }
+ let(:source) do
+ <<~SOURCE
+ def up
+ BackgroundMigrationWorker.perform_async(ClazzName, "Bar", "Baz")
+ end
+ SOURCE
+ end
+
+ shared_examples 'a disabled cop' do
+ it 'does not register any offenses' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of a migration' do
+ it_behaves_like 'a disabled cop'
+ end
+
+ context 'in a migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ context 'in an old migration' do
+ before do
+ allow(cop).to receive(:version).and_return(described_class::ENFORCED_SINCE - 5)
+ end
+
+ it_behaves_like 'a disabled cop'
+ end
+
+ context 'that is recent' do
+ before do
+ allow(cop).to receive(:version).and_return(described_class::ENFORCED_SINCE + 5)
+ end
+
+ context 'BackgroundMigrationWorker.perform_async' do
+ it 'adds an offence when calling `BackgroundMigrationWorker.peform_async`' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'autocorrects to the right version' do
+ correct_source = <<~CORRECT
+ def up
+ migrate_async(ClazzName, "Bar", "Baz")
+ end
+ CORRECT
+
+ expect(autocorrect_source(source)).to eq(correct_source)
+ end
+ end
+
+ context 'BackgroundMigrationWorker.perform_in' do
+ let(:source) do
+ <<~SOURCE
+ def up
+ BackgroundMigrationWorker
+ .perform_in(delay, ClazzName, "Bar", "Baz")
+ end
+ SOURCE
+ end
+
+ it 'adds an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'autocorrects to the right version' do
+ correct_source = <<~CORRECT
+ def up
+ migrate_in(delay, ClazzName, "Bar", "Baz")
+ end
+ CORRECT
+
+ expect(autocorrect_source(source)).to eq(correct_source)
+ end
+ end
+
+ context 'BackgroundMigrationWorker.bulk_perform_async' do
+ let(:source) do
+ <<~SOURCE
+ def up
+ BackgroundMigrationWorker
+ .bulk_perform_async(jobs)
+ end
+ SOURCE
+ end
+
+ it 'adds an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'autocorrects to the right version' do
+ correct_source = <<~CORRECT
+ def up
+ bulk_migrate_async(jobs)
+ end
+ CORRECT
+
+ expect(autocorrect_source(source)).to eq(correct_source)
+ end
+ end
+
+ context 'BackgroundMigrationWorker.bulk_perform_in' do
+ let(:source) do
+ <<~SOURCE
+ def up
+ BackgroundMigrationWorker
+ .bulk_perform_in(5.minutes, jobs)
+ end
+ SOURCE
+ end
+
+ it 'adds an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'autocorrects to the right version' do
+ correct_source = <<~CORRECT
+ def up
+ bulk_migrate_in(5.minutes, jobs)
+ end
+ CORRECT
+
+ expect(autocorrect_source(source)).to eq(correct_source)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/scalability/idempotent_worker_spec.rb b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
new file mode 100644
index 00000000000..7abd602f8bc
--- /dev/null
+++ b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/scalability/idempotent_worker'
+
+describe RuboCop::Cop::Scalability::IdempotentWorker do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ before do
+ allow(cop)
+ .to receive(:in_worker?)
+ .and_return(true)
+ end
+
+ it 'adds an offense when not defining idempotent method' do
+ inspect_source(<<~CODE.strip_indent)
+ class SomeWorker
+ end
+ CODE
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'adds an offense when not defining idempotent method' do
+ inspect_source(<<~CODE.strip_indent)
+ class SomeWorker
+ idempotent!
+ end
+ CODE
+
+ expect(cop.offenses.size).to be_zero
+ end
+end
diff --git a/spec/rubocop/migration_helpers_spec.rb b/spec/rubocop/migration_helpers_spec.rb
new file mode 100644
index 00000000000..73ced8c58da
--- /dev/null
+++ b/spec/rubocop/migration_helpers_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require 'rspec-parameterized'
+
+require_relative '../../rubocop/migration_helpers'
+
+describe RuboCop::MigrationHelpers do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:fake_cop) { Class.new { include RuboCop::MigrationHelpers }.new }
+
+ let(:node) { double(:node) }
+
+ before do
+ allow(node).to receive_message_chain('location.expression.source_buffer.name')
+ .and_return(name)
+ end
+
+ describe '#in_migration?' do
+ where(:name, :expected) do
+ '/gitlab/db/migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/post_migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/geo/migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/geo/post_migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/elsewhere/20200210184420_create_operations_scopes_table.rb' | false
+ end
+
+ with_them do
+ it { expect(fake_cop.in_migration?(node)).to eq(expected) }
+ end
+ end
+
+ describe '#in_post_deployment_migration?' do
+ where(:name, :expected) do
+ '/gitlab/db/migrate/20200210184420_create_operations_scopes_table.rb' | false
+ '/gitlab/db/post_migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/geo/migrate/20200210184420_create_operations_scopes_table.rb' | false
+ '/gitlab/db/geo/post_migrate/20200210184420_create_operations_scopes_table.rb' | true
+ '/gitlab/db/elsewhere/20200210184420_create_operations_scopes_table.rb' | false
+ end
+
+ with_them do
+ it { expect(fake_cop.in_post_deployment_migration?(node)).to eq(expected) }
+ end
+ end
+
+ describe "#version" do
+ let(:name) do
+ '/path/to/gitlab/db/migrate/20200210184420_create_operations_scopes_table.rb'
+ end
+
+ it { expect(fake_cop.version(node)).to eq(20200210184420) }
+ end
+end
diff --git a/spec/services/issuable/clone/attributes_rewriter_spec.rb b/spec/services/issuable/clone/attributes_rewriter_spec.rb
index 20bda6984bd..6bc0df8260b 100644
--- a/spec/services/issuable/clone/attributes_rewriter_spec.rb
+++ b/spec/services/issuable/clone/attributes_rewriter_spec.rb
@@ -75,5 +75,47 @@ describe Issuable::Clone::AttributesRewriter do
expect(new_issue.reload.milestone).to eq(milestone)
end
+
+ context 'with existing milestone events' do
+ let!(:milestone1_project1) { create(:milestone, title: 'milestone1', project: project1) }
+ let!(:milestone2_project1) { create(:milestone, title: 'milestone2', project: project1) }
+ let!(:milestone3_project1) { create(:milestone, title: 'milestone3', project: project1) }
+
+ let!(:milestone1_project2) { create(:milestone, title: 'milestone1', project: project2) }
+ let!(:milestone2_project2) { create(:milestone, title: 'milestone2', project: project2) }
+
+ before do
+ original_issue.update(milestone: milestone2_project1)
+
+ create_event(milestone1_project1)
+ create_event(milestone2_project1)
+ create_event(milestone1_project1, 'remove')
+ create_event(milestone3_project1)
+ end
+
+ it 'copies existing resource milestone events' do
+ subject.execute
+
+ new_issue_milestone_events = new_issue.reload.resource_milestone_events
+ expect(new_issue_milestone_events.count).to eq(3)
+
+ expect_milestone_event(new_issue_milestone_events.first, milestone: milestone1_project2, action: 'add', state: 'opened')
+ expect_milestone_event(new_issue_milestone_events.second, milestone: milestone2_project2, action: 'add', state: 'opened')
+ expect_milestone_event(new_issue_milestone_events.third, milestone: milestone1_project2, action: 'remove', state: 'opened')
+ end
+
+ def create_event(milestone, action = 'add')
+ create(:resource_milestone_event, issue: original_issue, milestone: milestone, action: action)
+ end
+
+ def expect_milestone_event(event, expected_attrs)
+ expect(event.milestone_id).to eq(expected_attrs[:milestone].id)
+ expect(event.action).to eq(expected_attrs[:action])
+ expect(event.state).to eq(expected_attrs[:state])
+
+ expect(event.reference).to be_nil
+ expect(event.reference_html).to be_nil
+ end
+ end
end
end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index 7e40ac9ff4d..771e7ca42c9 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
describe Issuable::CommonSystemNotesService do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
let(:issuable) { create(:issue, project: project) }
context 'on issuable update' do
@@ -35,6 +36,8 @@ describe Issuable::CommonSystemNotesService do
before do
milestone = create(:milestone, project: project)
issuable.milestone_id = milestone.id
+
+ stub_feature_flags(track_resource_milestone_change_events: false)
end
it_behaves_like 'system note creation', {}, 'changed milestone'
@@ -97,12 +100,39 @@ describe Issuable::CommonSystemNotesService do
expect(event.user_id).to eq user.id
end
- it 'creates a system note for milestone set' do
- issuable.milestone = create(:milestone, project: project)
- issuable.save
+ context 'when milestone change event tracking is disabled' do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
- expect { subject }.to change { issuable.notes.count }.from(0).to(1)
- expect(issuable.notes.last.note).to match('changed milestone')
+ issuable.milestone = create(:milestone, project: project)
+ issuable.save
+ end
+
+ it 'creates a system note for milestone set' do
+ expect { subject }.to change { issuable.notes.count }.from(0).to(1)
+ expect(issuable.notes.last.note).to match('changed milestone')
+ end
+
+ it 'does not create a milestone change event' do
+ expect { subject }.not_to change { ResourceMilestoneEvent.count }
+ end
+ end
+
+ context 'when milestone change event tracking is enabled' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issuable) { create(:issue, project: project, milestone: milestone) }
+
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: true)
+ end
+
+ it 'does not create a system note for milestone set' do
+ expect { subject }.not_to change { issuable.notes.count }
+ end
+
+ it 'creates a milestone change event' do
+ expect { subject }.to change { ResourceMilestoneEvent.count }.from(0).to(1)
+ end
end
it 'creates a system note for due_date set' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 888a63980f6..5da77dd914c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -385,6 +385,10 @@ describe Issues::UpdateService, :mailer do
end
context 'when the milestone is removed' do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
+ end
+
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
@@ -411,6 +415,10 @@ describe Issues::UpdateService, :mailer do
end
context 'when the milestone is changed' do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
+ end
+
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index f295f3c4a81..dd5d90b2d07 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -367,6 +367,10 @@ describe MergeRequests::UpdateService, :mailer do
end
context 'when the milestone is removed' do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
+ end
+
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
@@ -393,6 +397,10 @@ describe MergeRequests::UpdateService, :mailer do
end
context 'when the milestone is changed' do
+ before do
+ stub_feature_flags(track_resource_milestone_change_events: false)
+ end
+
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
new file mode 100644
index 00000000000..c6b4f8e1b7e
--- /dev/null
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceEvents::ChangeMilestoneService do
+ shared_examples 'milestone events creator' do
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:milestone) { create(:milestone) }
+
+ context 'when milestone is present' do
+ before do
+ resource.milestone = milestone
+ end
+
+ let(:service) { described_class.new(resource: resource, user: user, created_at: created_at_time) }
+
+ it 'creates the expected event record' do
+ expect { service.execute }.to change { ResourceMilestoneEvent.count }.from(0).to(1)
+
+ events = ResourceMilestoneEvent.all
+
+ expect(events.size).to eq(1)
+ expect_event_record(events.first, action: 'add', milestone: milestone, state: 'opened')
+ end
+ end
+
+ context 'when milestones is not present' do
+ before do
+ resource.milestone = nil
+ end
+
+ let(:service) { described_class.new(resource: resource, user: user, created_at: created_at_time) }
+
+ it 'creates the expected event records' do
+ expect { service.execute }.to change { ResourceMilestoneEvent.count }.from(0).to(1)
+
+ expect_event_record(ResourceMilestoneEvent.first, action: 'remove', milestone: nil, state: 'opened')
+ end
+ end
+
+ def expect_event_record(event, expected_attrs)
+ expect(event.action).to eq(expected_attrs[:action])
+ expect(event.state).to eq(expected_attrs[:state])
+ expect(event.user).to eq(user)
+ expect(event.issue).to eq(resource) if resource.is_a?(Issue)
+ expect(event.issue).to be_nil unless resource.is_a?(Issue)
+ expect(event.merge_request).to eq(resource) if resource.is_a?(MergeRequest)
+ expect(event.merge_request).to be_nil unless resource.is_a?(MergeRequest)
+ expect(event.milestone).to eq(expected_attrs[:milestone])
+ expect(event.created_at).to eq(created_at_time)
+ end
+ end
+
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:issue) { create(:issue) }
+
+ let!(:created_at_time) { Time.utc(2019, 12, 30) }
+
+ it_behaves_like 'milestone events creator' do
+ let(:resource) { issue }
+ end
+
+ it_behaves_like 'milestone events creator' do
+ let(:resource) { merge_request }
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 35bf6846ab3..b862fc4bbd9 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -119,6 +119,7 @@ RSpec.configure do |config|
config.include PolicyHelpers, type: :policy
config.include MemoryUsageHelper
config.include ExpectRequestWithStatus, type: :request
+ config.include IdempotentWorkerHelper, type: :worker
config.include RailsHelpers
if ENV['CI'] || ENV['RETRIES']
diff --git a/spec/support/helpers/idempotent_worker_helper.rb b/spec/support/helpers/idempotent_worker_helper.rb
new file mode 100644
index 00000000000..b80758d10bd
--- /dev/null
+++ b/spec/support/helpers/idempotent_worker_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module IdempotentWorkerHelper
+ WORKER_EXEC_TIMES = 2
+
+ def perform_multiple(args = [], worker: described_class.new, exec_times: WORKER_EXEC_TIMES)
+ Sidekiq::Testing.inline! do
+ job_args = args.nil? ? [nil] : Array.wrap(args)
+
+ expect(worker).to receive(:perform).exactly(exec_times).and_call_original
+
+ exec_times.times { worker.perform(*job_args) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
index 897a962fc56..32c46753006 100644
--- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -50,6 +50,8 @@ RSpec.shared_examples 'move quick action' do
let(:bug) { create(:label, project: project, title: 'bug') }
let(:wontfix) { create(:label, project: project, title: 'wontfix') }
+ let!(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
+
before do
target_project.add_maintainer(user)
end
diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb
new file mode 100644
index 00000000000..d7e7349ad6c
--- /dev/null
+++ b/spec/support/shared_examples/resource_events.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples 'a resource event' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, author: user1) }
+ let_it_be(:issue2) { create(:issue, author: user1) }
+ let_it_be(:issue3) { create(:issue, author: user2) }
+
+ describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:user) }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe '.created_after' do
+ let!(:created_at1) { 1.day.ago }
+ let!(:created_at2) { 2.days.ago }
+ let!(:created_at3) { 3.days.ago }
+
+ let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) }
+ let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) }
+ let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) }
+
+ it 'returns the expected events' do
+ events = described_class.created_after(created_at3)
+
+ expect(events).to contain_exactly(event1, event2)
+ end
+
+ it 'returns no events if time is after last record time' do
+ events = described_class.created_after(1.minute.ago)
+
+ expect(events).to be_empty
+ end
+ end
+end
+
+shared_examples 'a resource event for issues' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, author: user1) }
+ let_it_be(:issue2) { create(:issue, author: user1) }
+ let_it_be(:issue3) { create(:issue, author: user2) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe '.by_issue' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_issue(issue1)
+
+ expect(events).to contain_exactly(event1, event3)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_issue(issue3)
+
+ expect(events).to be_empty
+ end
+ end
+end
+
+shared_examples 'a resource event for merge requests' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:merge_request1) { create(:merge_request, author: user1) }
+ let_it_be(:merge_request2) { create(:merge_request, author: user1) }
+ let_it_be(:merge_request3) { create(:merge_request, author: user2) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:merge_request) }
+ end
+
+ describe '.by_merge_request' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_merge_request(merge_request1)
+
+ expect(events).to contain_exactly(event1, event3)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_merge_request(merge_request3)
+
+ expect(events).to be_empty
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/idempotency_shared_examples.rb b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
new file mode 100644
index 00000000000..19be1fe2c9d
--- /dev/null
+++ b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - job_args (if not given, will fallback to call perform without arguments)
+#
+# Usage:
+#
+# include_examples 'an idempotent worker' do
+# it 'checks the side-effects for multiple calls' do
+# # it'll call the job's perform method 3 times
+# # by default.
+# subject
+#
+# expect(model.state).to eq('state')
+# end
+# end
+#
+RSpec.shared_examples 'an idempotent worker' do
+ # Avoid stubbing calls for a more accurate run.
+ subject do
+ defined?(job_args) ? perform_multiple(job_args) : perform_multiple
+ end
+
+ it 'is labeled as idempotent' do
+ expect(described_class).to be_idempotent
+ end
+
+ it 'performs multiple times sequentially without raising an exception' do
+ expect { subject }.not_to raise_error
+ end
+end
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index aae6fa02a0c..2b2ffc9f5b9 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -11,7 +11,7 @@ describe BackgroundMigrationWorker, :clean_gitlab_redis_shared_state do
end
end
- describe '.perform' do
+ describe '#perform' do
it 'performs a background migration' do
expect(Gitlab::BackgroundMigration)
.to receive(:perform)
@@ -52,6 +52,14 @@ describe BackgroundMigrationWorker, :clean_gitlab_redis_shared_state do
worker.perform('Foo', [10, 20])
end
+
+ it 'sets the class that will be executed as the caller_id' do
+ expect(Gitlab::BackgroundMigration).to receive(:perform) do
+ expect(Labkit::Context.current.to_h).to include('meta.caller_id' => 'Foo')
+ end
+
+ worker.perform('Foo', [10, 20])
+ end
end
describe '#healthy_database?' do
diff --git a/spec/workers/expire_job_cache_worker_spec.rb b/spec/workers/expire_job_cache_worker_spec.rb
index eeab304d926..ebca7020ee5 100644
--- a/spec/workers/expire_job_cache_worker_spec.rb
+++ b/spec/workers/expire_job_cache_worker_spec.rb
@@ -6,20 +6,32 @@ describe ExpireJobCacheWorker do
set(:pipeline) { create(:ci_empty_pipeline) }
let(:project) { pipeline.project }
- subject { described_class.new }
-
describe '#perform' do
context 'with a job in the pipeline' do
let(:job) { create(:ci_build, pipeline: pipeline) }
+ let(:job_args) { job.id }
+
+ include_examples 'an idempotent worker' do
+ it 'invalidates Etag caching for the job path' do
+ pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json"
+ job_path = "/#{project.full_path}/builds/#{job.id}.json"
+
+ spy_store = Gitlab::EtagCaching::Store.new
+
+ allow(Gitlab::EtagCaching::Store).to receive(:new) { spy_store }
- it 'invalidates Etag caching for the job path' do
- pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json"
- job_path = "/#{project.full_path}/builds/#{job.id}.json"
+ expect(spy_store).to receive(:touch)
+ .exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES).times
+ .with(pipeline_path)
+ .and_call_original
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipeline_path)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(job_path)
+ expect(spy_store).to receive(:touch)
+ .exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES).times
+ .with(job_path)
+ .and_call_original
- subject.perform(job.id)
+ subject
+ end
end
end
@@ -27,7 +39,7 @@ describe ExpireJobCacheWorker do
it 'does not change the etag store' do
expect(Gitlab::EtagCaching::Store).not_to receive(:new)
- subject.perform(9999)
+ perform_multiple(9999)
end
end
end