diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-06 21:09:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-06 21:09:02 +0300 |
commit | 2d181003830956f5e690cce74be50bb4d96048f8 (patch) | |
tree | cd72c53f6cb3753fc1bd28521e89af66c420f08a /spec | |
parent | c79da5142f46e6e9187f75c329e2c81a8568c581 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/components/pajamas/banner_component_spec.rb | 169 | ||||
-rw-r--r-- | spec/controllers/autocomplete_controller_spec.rb | 2 | ||||
-rw-r--r-- | spec/events/pages/page_deleted_event_spec.rb | 34 | ||||
-rw-r--r-- | spec/graphql/types/ci/pipeline_merge_request_event_type_enum_spec.rb | 14 | ||||
-rw-r--r-- | spec/graphql/types/ci/pipeline_type_spec.rb | 2 | ||||
-rw-r--r-- | spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb | 92 | ||||
-rw-r--r-- | spec/lib/gitlab/mailgun/webhook_processors/member_invites_spec.rb | 74 | ||||
-rw-r--r-- | spec/serializers/deploy_keys/basic_deploy_key_entity_spec.rb (renamed from spec/serializers/deploy_key_entity_spec.rb) | 18 | ||||
-rw-r--r-- | spec/serializers/deploy_keys/deploy_key_entity_spec.rb | 51 | ||||
-rw-r--r-- | spec/services/members/mailgun/process_webhook_service_spec.rb | 72 | ||||
-rw-r--r-- | spec/services/pages/delete_service_spec.rb | 6 | ||||
-rw-r--r-- | spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb | 60 |
12 files changed, 479 insertions, 115 deletions
diff --git a/spec/components/pajamas/banner_component_spec.rb b/spec/components/pajamas/banner_component_spec.rb new file mode 100644 index 00000000000..5969f06dbad --- /dev/null +++ b/spec/components/pajamas/banner_component_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Pajamas::BannerComponent, type: :component do + subject do + described_class.new(**options) + end + + let(:title) { "Banner title" } + let(:content) { "Banner content"} + let(:options) { {} } + + describe 'basic usage' do + before do + render_inline(subject) do |c| + c.title { title } + content + end + end + + it 'renders its content' do + expect(rendered_component).to have_text content + end + + it 'renders its title' do + expect(rendered_component).to have_css "h1[class='gl-banner-title']", text: title + end + + it 'renders a close button' do + expect(rendered_component).to have_css "button.gl-banner-close" + end + + describe 'button_text and button_link' do + let(:options) { { button_text: 'Learn more', button_link: '/learn-more' } } + + it 'define the primary action' do + expect(rendered_component).to have_css "a.btn-confirm.gl-button[href='/learn-more']", text: 'Learn more' + end + end + + describe 'banner_options' do + let(:options) { { banner_options: { class: "baz", data: { foo: "bar" } } } } + + it 'are on the banner' do + expect(rendered_component).to have_css ".gl-banner.baz[data-foo='bar']" + end + + context 'with custom classes' do + let(:options) { { variant: :introduction, banner_options: { class: 'extra special' } } } + + it 'don\'t conflict with internal banner_classes' do + expect(rendered_component).to have_css '.extra.special.gl-banner-introduction.gl-banner' + end + end + end + + describe 'close_options' do + let(:options) { { close_options: { class: "js-foo", data: { uid: "123" } } } } + + it 'are on the close button' do + expect(rendered_component).to have_css "button.gl-banner-close.js-foo[data-uid='123']" + end + end + + describe 'embedded' do + context 'by default (false)' do + it 'keeps the banner\'s borders' do + expect(rendered_component).not_to have_css ".gl-banner.gl-border-none" + end + end + + context 'when set to true' do + let(:options) { { embedded: true } } + + it 'removes the banner\'s borders' do + expect(rendered_component).to have_css ".gl-banner.gl-border-none" + end + end + end + + describe 'variant' do + context 'by default (promotion)' do + it 'applies no variant class' do + expect(rendered_component).to have_css "[class='gl-banner']" + end + end + + context 'when set to introduction' do + let(:options) { { variant: :introduction } } + + it "applies the introduction class to the banner" do + expect(rendered_component).to have_css ".gl-banner.gl-banner-introduction" + end + + it "applies the confirm class to the close button" do + expect(rendered_component).to have_css ".gl-banner-close.btn-confirm.btn-confirm-tertiary" + end + end + + context 'when set to unknown variant' do + let(:options) { { variant: :foobar } } + + it 'ignores the unknown variant' do + expect(rendered_component).to have_css "[class='gl-banner']" + end + end + end + + describe 'illustration' do + it 'has none by default' do + expect(rendered_component).not_to have_css ".gl-banner-illustration" + end + + context 'with svg_path' do + let(:options) { { svg_path: 'logo.svg' } } + + it 'renders an image as illustration' do + expect(rendered_component).to have_css ".gl-banner-illustration img" + end + end + end + end + + context 'with illustration slot' do + before do + render_inline(subject) do |c| + c.title { title } + c.illustration { "<svg></svg>".html_safe } + content + end + end + + it 'renders the slot content as illustration' do + expect(rendered_component).to have_css ".gl-banner-illustration svg" + end + + context 'and conflicting svg_path' do + let(:options) { { svg_path: 'logo.svg' } } + + it 'uses the slot content' do + expect(rendered_component).to have_css ".gl-banner-illustration svg" + expect(rendered_component).not_to have_css ".gl-banner-illustration img" + end + end + end + + context 'with primary_action slot' do + before do + render_inline(subject) do |c| + c.title { title } + c.primary_action { "<a class='special' href='#'>Special</a>".html_safe } + content + end + end + + it 'renders the slot content as the primary action' do + expect(rendered_component).to have_css "a.special", text: 'Special' + end + + context 'and conflicting button_text and button_link' do + let(:options) { { button_text: 'Not special', button_link: '/' } } + + it 'uses the slot content' do + expect(rendered_component).to have_css "a.special[href='#']", text: 'Special' + expect(rendered_component).not_to have_css "a.btn[href='/']" + end + end + end +end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 0a809e80fcd..e874df62cd7 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -411,6 +411,7 @@ RSpec.describe AutocompleteController do expect(json_response.count).to eq(1) expect(json_response.first['title']).to eq(deploy_key.title) expect(json_response.first['owner']['id']).to eq(deploy_key.user.id) + expect(json_response.first['deploy_keys_projects']).to be_nil end context 'with an unknown project' do @@ -433,6 +434,7 @@ RSpec.describe AutocompleteController do expect(json_response.count).to eq(1) expect(json_response.first['title']).to eq(deploy_key.title) expect(json_response.first['owner']).to be_nil + expect(json_response.first['deploy_keys_projects']).to be_nil end end end diff --git a/spec/events/pages/page_deleted_event_spec.rb b/spec/events/pages/page_deleted_event_spec.rb new file mode 100644 index 00000000000..ee05b770c48 --- /dev/null +++ b/spec/events/pages/page_deleted_event_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Pages::PageDeletedEvent do + where(:data, :valid) do + [ + [{ project_id: 1, namespace_id: 2 }, true], + [{ project_id: 1 }, false], + [{ namespace_id: 1 }, false], + [{ project_id: 'foo', namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: 'foo' }, false], + [{ project_id: [], namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: [] }, false], + [{ project_id: {}, namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: {} }, false], + ['foo', false], + [123, false], + [[], false] + ] + end + + with_them do + it 'validates data' do + constructor = -> { described_class.new(data: data) } + + if valid + expect { constructor.call }.not_to raise_error + else + expect { constructor.call }.to raise_error(Gitlab::EventStore::InvalidEvent) + end + end + end +end diff --git a/spec/graphql/types/ci/pipeline_merge_request_event_type_enum_spec.rb b/spec/graphql/types/ci/pipeline_merge_request_event_type_enum_spec.rb new file mode 100644 index 00000000000..3a90e4f1fd9 --- /dev/null +++ b/spec/graphql/types/ci/pipeline_merge_request_event_type_enum_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PipelineMergeRequestEventType'] do + specify { expect(described_class.graphql_name).to eq('PipelineMergeRequestEventType') } + + it 'has specific values' do + expect(described_class.values).to match a_hash_including( + 'MERGED_RESULT' => have_attributes(value: :merged_result), + 'DETACHED' => have_attributes(value: :detached) + ) + end +end diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index 94d1b42da37..9dee834d05f 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Types::Ci::PipelineType do coverage created_at updated_at started_at finished_at committed_at stages user retryable cancelable jobs source_job job job_artifacts downstream upstream path project active user_permissions warnings commit commit_path uses_needs - test_report_summary test_suite ref ref_path warning_messages + test_report_summary test_suite ref ref_path warning_messages merge_request_event_type ] if Gitlab.ee? diff --git a/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb new file mode 100644 index 00000000000..a2286415e96 --- /dev/null +++ b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Mailgun::WebhookProcessors::FailureLogger do + describe '#execute', :freeze_time, :clean_gitlab_redis_rate_limiting do + let(:base_payload) do + { + 'id' => 'U2kZkAiuScqcMTq-8Atz-Q', + 'event' => 'failed', + 'recipient' => 'recipient@gitlab.com', + 'reason' => 'bounce', + 'delivery-status' => { + 'code' => '421', + 'message' => '4.4.2 mxfront9g.mail.example.com Error: timeout exceeded' + } + } + end + + context 'on permanent failure' do + let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) } + + it 'logs the failure immediately' do + expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( + event: 'email_delivery_failure', + mailgun_event_id: base_payload['id'], + recipient: base_payload['recipient'], + failure_type: 'permanent', + failure_reason: base_payload['reason'], + failure_code: base_payload['delivery-status']['code'], + failure_message: base_payload['delivery-status']['message'] + ) + + processor.execute + end + end + + context 'on temporary failure' do + let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'temporary' })) } + + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits) + .and_return(temporary_email_failure: { threshold: 1, interval: 1.minute }) + end + + context 'when threshold is not exceeded' do + it 'increments counter but does not log the failure' do + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with( + :temporary_email_failure, scope: 'recipient@gitlab.com' + ).and_call_original + expect(Gitlab::ErrorTracking::Logger).not_to receive(:error) + + processor.execute + end + end + + context 'when threshold is exceeded' do + before do + processor.execute + end + + it 'increments counter and logs the failure' do + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with( + :temporary_email_failure, scope: 'recipient@gitlab.com' + ).and_call_original + expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( + event: 'email_delivery_failure', + mailgun_event_id: base_payload['id'], + recipient: base_payload['recipient'], + failure_type: 'temporary', + failure_reason: base_payload['reason'], + failure_code: base_payload['delivery-status']['code'], + failure_message: base_payload['delivery-status']['message'] + ) + + processor.execute + end + end + end + + context 'on other events' do + let(:processor) { described_class.new(base_payload.merge({ 'event' => 'delivered' })) } + + it 'does nothing' do + expect(Gitlab::ErrorTracking::Logger).not_to receive(:error) + expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + + processor.execute + end + end + end +end diff --git a/spec/lib/gitlab/mailgun/webhook_processors/member_invites_spec.rb b/spec/lib/gitlab/mailgun/webhook_processors/member_invites_spec.rb new file mode 100644 index 00000000000..3bd364b0d15 --- /dev/null +++ b/spec/lib/gitlab/mailgun/webhook_processors/member_invites_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Mailgun::WebhookProcessors::MemberInvites do + describe '#execute', :aggregate_failures do + let_it_be(:member) { create(:project_member, :invited) } + + let(:raw_invite_token) { member.raw_invite_token } + let(:payload) do + { + 'event' => 'failed', + 'severity' => 'permanent', + 'tags' => [Members::Mailgun::INVITE_EMAIL_TAG], + 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } + } + end + + subject(:service) { described_class.new(payload).execute } + + it 'marks the member invite email success as false' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: /^UPDATED MEMBER INVITE_EMAIL_SUCCESS/, + event: 'updated_member_invite_email_success' + ).and_call_original + + expect { service }.to change { member.reload.invite_email_success }.from(true).to(false) + end + + context 'when invite token is not found in payload' do + before do + payload.delete('user-variables') + end + + it 'does not change member status and logs an error' do + expect(Gitlab::AppLogger).not_to receive(:info) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + an_instance_of(described_class::ProcessWebhookServiceError)) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + + shared_examples 'does nothing' do + it 'does not change member status' do + expect(Gitlab::AppLogger).not_to receive(:info) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + + context 'when member can not be found' do + let(:raw_invite_token) { '_foobar_' } + + it_behaves_like 'does nothing' + end + + context 'when failure is temporary' do + before do + payload['severity'] = 'temporary' + end + + it_behaves_like 'does nothing' + end + + context 'when email is not a member invite' do + before do + payload.delete('tags') + end + + it_behaves_like 'does nothing' + end + end +end diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_keys/basic_deploy_key_entity_spec.rb index e8d9701be67..c39eb14e339 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_keys/basic_deploy_key_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe DeployKeyEntity do +RSpec.describe DeployKeys::BasicDeployKeyEntity do include RequestAwareEntity let(:user) { create(:user) } @@ -18,7 +18,7 @@ RSpec.describe DeployKeyEntity do project_private.deploy_keys << deploy_key end - describe 'returns deploy keys with projects a user can read' do + describe 'returns deploy keys' do let(:expected_result) do { id: deploy_key.id, @@ -30,19 +30,7 @@ RSpec.describe DeployKeyEntity do almost_orphaned: false, created_at: deploy_key.created_at, updated_at: deploy_key.updated_at, - can_edit: false, - deploy_keys_projects: [ - { - can_push: false, - project: - { - id: project.id, - name: project.name, - full_path: project_path(project), - full_name: project.full_name - } - } - ] + can_edit: false } end diff --git a/spec/serializers/deploy_keys/deploy_key_entity_spec.rb b/spec/serializers/deploy_keys/deploy_key_entity_spec.rb new file mode 100644 index 00000000000..e989aa8656c --- /dev/null +++ b/spec/serializers/deploy_keys/deploy_key_entity_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DeployKeys::DeployKeyEntity do + include RequestAwareEntity + + let(:user) { create(:user) } + let(:project) { create(:project, :internal)} + let(:project_private) { create(:project, :private)} + let(:deploy_key) { create(:deploy_key) } + let(:options) { { user: user } } + + let(:entity) { described_class.new(deploy_key, options) } + + before do + project.deploy_keys << deploy_key + project_private.deploy_keys << deploy_key + end + + describe 'returns deploy keys with projects a user can read' do + let(:expected_result) do + { + id: deploy_key.id, + user_id: deploy_key.user_id, + title: deploy_key.title, + fingerprint: deploy_key.fingerprint, + fingerprint_sha256: deploy_key.fingerprint_sha256, + destroyed_when_orphaned: true, + almost_orphaned: false, + created_at: deploy_key.created_at, + updated_at: deploy_key.updated_at, + can_edit: false, + deploy_keys_projects: [ + { + can_push: false, + project: + { + id: project.id, + name: project.name, + full_path: project_path(project), + full_name: project.full_name + } + } + ] + } + end + + it { expect(entity.as_json).to eq(expected_result) } + end +end diff --git a/spec/services/members/mailgun/process_webhook_service_spec.rb b/spec/services/members/mailgun/process_webhook_service_spec.rb deleted file mode 100644 index 3b657c05bd8..00000000000 --- a/spec/services/members/mailgun/process_webhook_service_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Members::Mailgun::ProcessWebhookService do - describe '#execute', :aggregate_failures do - let_it_be(:member) { create(:project_member, :invited) } - - let(:raw_invite_token) { member.raw_invite_token } - let(:payload) { { 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } } } - - subject(:service) { described_class.new(payload).execute } - - it 'marks the member invite email success as false' do - expect(Gitlab::AppLogger).to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/).and_call_original - - expect { service }.to change { member.reload.invite_email_success }.from(true).to(false) - end - - context 'when member can not be found' do - let(:raw_invite_token) { '_foobar_' } - - it 'does not change member status' do - expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) - - expect { service }.not_to change { member.reload.invite_email_success } - end - end - - context 'when invite token is not found in payload' do - let(:payload) { {} } - - it 'does not change member status and logs an error' do - expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - an_instance_of(described_class::ProcessWebhookServiceError)) - - expect { service }.not_to change { member.reload.invite_email_success } - end - end - end - - describe '#should_process?' do - it 'processes permanent failures for member invite emails' do - payload = { 'event' => 'failed', 'severity' => 'permanent', 'tags' => [Members::Mailgun::INVITE_EMAIL_TAG] } - service = described_class.new(payload) - - expect(service.should_process?).to eq(true) - end - - it 'does not process temporary failures' do - payload = { 'event' => 'failed', 'severity' => 'temporary', 'tags' => [Members::Mailgun::INVITE_EMAIL_TAG] } - service = described_class.new(payload) - - expect(service.should_process?).to eq(false) - end - - it 'does not process non member invite emails' do - payload = { 'event' => 'failed', 'severity' => 'permanent', 'tags' => [] } - service = described_class.new(payload) - - expect(service.should_process?).to eq(false) - end - - it 'does not process other types of events' do - payload = { 'event' => 'delivered', 'tags' => [Members::Mailgun::INVITE_EMAIL_TAG] } - service = described_class.new(payload) - - expect(service.should_process?).to eq(false) - end - end -end diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb index e02e8e72e0b..0c0b2c0431b 100644 --- a/spec/services/pages/delete_service_spec.rb +++ b/spec/services/pages/delete_service_spec.rb @@ -43,4 +43,10 @@ RSpec.describe Pages::DeleteService do service.execute end.to change { PagesDeployment.count }.by(-1) end + + it 'publishes a ProjectDeleted event with project id and namespace id' do + expected_data = { project_id: project.id, namespace_id: project.namespace_id } + + expect { service.execute }.to publish_event(Pages::PageDeletedEvent).with(expected_data) + end end diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb index da9c5833926..ec10c66968d 100644 --- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -5,6 +5,34 @@ require 'spec_helper' RSpec.describe Projects::InactiveProjectsDeletionCronWorker do include ProjectHelpers + shared_examples 'worker is running for more than 4 minutes' do + before do + subject.instance_variable_set(:@start_time, ::Gitlab::Metrics::System.monotonic_time - 5.minutes) + end + + it 'stores the last processed inactive project_id in redis cache' do + Gitlab::Redis::Cache.with do |redis| + expect { worker.perform } + .to change { redis.get('last_processed_inactive_project_id') }.to(inactive_large_project.id.to_s) + end + end + end + + shared_examples 'worker finishes processing in less than 4 minutes' do + before do + Gitlab::Redis::Cache.with do |redis| + redis.set('last_processed_inactive_project_id', inactive_large_project.id) + end + end + + it 'clears the last processed inactive project_id from redis cache' do + Gitlab::Redis::Cache.with do |redis| + expect { worker.perform } + .to change { redis.get('last_processed_inactive_project_id') }.to(nil) + end + end + end + describe "#perform" do subject(:worker) { described_class.new } @@ -79,6 +107,9 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker do expect(inactive_large_project.reload.pending_delete).to eq(false) end + + it_behaves_like 'worker is running for more than 4 minutes' + it_behaves_like 'worker finishes processing in less than 4 minutes' end context 'when feature flag is enabled', :clean_gitlab_redis_shared_state, :sidekiq_inline do @@ -130,33 +161,8 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker do end end - context 'when the worker is running for more than 4 minutes' do - before do - subject.instance_variable_set(:@start_time, ::Gitlab::Metrics::System.monotonic_time - 5.minutes) - end - - it 'stores the last processed inactive project_id in redis cache' do - Gitlab::Redis::Cache.with do |redis| - expect { worker.perform } - .to change { redis.get('last_processed_inactive_project_id') }.to(inactive_large_project.id.to_s) - end - end - end - - context 'when the worker finishes processing in less than 4 minutes' do - before do - Gitlab::Redis::Cache.with do |redis| - redis.set('last_processed_inactive_project_id', inactive_large_project.id) - end - end - - it 'clears the last processed inactive project_id from redis cache' do - Gitlab::Redis::Cache.with do |redis| - expect { worker.perform } - .to change { redis.get('last_processed_inactive_project_id') }.to(nil) - end - end - end + it_behaves_like 'worker is running for more than 4 minutes' + it_behaves_like 'worker finishes processing in less than 4 minutes' end it_behaves_like 'an idempotent worker' |