Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-03 12:07:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-03 12:07:33 +0300
commitc0d8f9f3f962df6bfcc70440432da55d67307189 (patch)
tree457666705fbbd4f517d201680113406163829fcc /spec
parent2cfa1fc75dd4bd6d1f70d5fee1a824410694f297 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/profiles/active_sessions_spec.rb27
-rw-r--r--spec/features/projects/environments/environment_spec.rb38
-rw-r--r--spec/finders/pipelines_finder_spec.rb13
-rw-r--r--spec/frontend/environments/environment_item_spec.js77
-rw-r--r--spec/frontend/environments/environment_pin_spec.js46
-rw-r--r--spec/frontend/environments/mock_data.js5
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml14
-rw-r--r--spec/models/active_session_spec.rb50
-rw-r--r--spec/models/ci/pipeline_spec.rb110
-rw-r--r--spec/models/concerns/issuable_spec.rb187
-rw-r--r--spec/models/concerns/milestoneable_spec.rb243
-rw-r--r--spec/models/user_preference_spec.rb6
-rw-r--r--spec/requests/api/notes_spec.rb69
-rw-r--r--spec/services/ci/create_pipeline_service/custom_config_content_spec.rb29
-rw-r--r--spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb116
-rw-r--r--spec/workers/concerns/reenqueuer_spec.rb179
17 files changed, 1073 insertions, 189 deletions
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index a5c2d15f598..bab6251a5d4 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -84,4 +84,31 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
expect(page).not_to have_content('Chrome on Windows')
end
end
+
+ it 'User can revoke a session', :js, :redis_session_store do
+ Capybara::Session.new(:session1)
+ Capybara::Session.new(:session2)
+
+ # set an additional session in another browser
+ using_session :session2 do
+ gitlab_sign_in(user)
+ end
+
+ using_session :session1 do
+ gitlab_sign_in(user)
+ visit profile_active_sessions_path
+
+ expect(page).to have_link('Revoke', count: 1)
+
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).not_to have_link('Revoke')
+ end
+
+ using_session :session2 do
+ visit profile_active_sessions_path
+
+ expect(page).to have_content('You need to sign in or sign up before continuing.')
+ end
+ end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 55c6aed19e0..bbd33225bb9 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -12,6 +12,10 @@ describe 'Environment' do
project.add_role(user, role)
end
+ def auto_stop_button_selector
+ %q{button[title="Prevent environment from auto-stopping"]}
+ end
+
describe 'environment details page' do
let!(:environment) { create(:environment, project: project) }
let!(:permissions) { }
@@ -27,6 +31,40 @@ describe 'Environment' do
expect(page).to have_content(environment.name)
end
+ context 'without auto-stop' do
+ it 'does not show auto-stop text' do
+ expect(page).not_to have_content('Auto stops')
+ end
+
+ it 'does not show auto-stop button' do
+ expect(page).not_to have_selector(auto_stop_button_selector)
+ end
+ end
+
+ context 'with auto-stop' do
+ let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
+
+ before do
+ visit_environment(environment)
+ end
+
+ it 'shows auto stop info' do
+ expect(page).to have_content('Auto stops')
+ end
+
+ it 'shows auto stop button' do
+ expect(page).to have_selector(auto_stop_button_selector)
+ expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
+ end
+
+ it 'allows user to cancel auto stop', :js do
+ page.find(auto_stop_button_selector).click
+ wait_for_all_requests
+ expect(page).to have_content('Auto stop successfully canceled.')
+ expect(page).not_to have_selector(auto_stop_button_selector)
+ end
+ end
+
context 'without deployments' do
it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index c8a4ea799c3..1dbf9491118 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -64,6 +64,19 @@ describe PipelinesFinder do
end
end
+ context 'when project has child pipelines' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
+
+ let!(:pipeline_source) do
+ create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
+ end
+
+ it 'filters out child pipelines and show only the parents' do
+ is_expected.to eq([parent_pipeline])
+ end
+ end
+
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 52625c64a1c..004687fcf44 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -1,6 +1,8 @@
import { mount } from '@vue/test-utils';
import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
+import PinComponent from '~/environments/components/environment_pin.vue';
+
import { environment, folder, tableData } from './mock_data';
describe('Environment item', () => {
@@ -26,6 +28,8 @@ describe('Environment item', () => {
});
});
+ const findAutoStop = () => wrapper.find('.js-auto-stop');
+
afterEach(() => {
wrapper.destroy();
});
@@ -77,6 +81,79 @@ describe('Environment item', () => {
expect(wrapper.find('.js-commit-component')).toBeDefined();
});
});
+
+ describe('Without auto-stop date', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ model: environment,
+ canReadEnvironment: true,
+ tableData,
+ shouldShowAutoStopDate: true,
+ },
+ });
+ });
+
+ it('should not render a date', () => {
+ expect(findAutoStop().exists()).toBe(false);
+ });
+
+ it('should not render the suto-stop button', () => {
+ expect(wrapper.find(PinComponent).exists()).toBe(false);
+ });
+ });
+
+ describe('With auto-stop date', () => {
+ describe('in the future', () => {
+ const futureDate = new Date(Date.now() + 100000);
+ beforeEach(() => {
+ factory({
+ propsData: {
+ model: {
+ ...environment,
+ auto_stop_at: futureDate,
+ },
+ canReadEnvironment: true,
+ tableData,
+ shouldShowAutoStopDate: true,
+ },
+ });
+ });
+
+ it('renders the date', () => {
+ expect(findAutoStop().text()).toContain(format(futureDate));
+ });
+
+ it('should render the auto-stop button', () => {
+ expect(wrapper.find(PinComponent).exists()).toBe(true);
+ });
+ });
+
+ describe('in the past', () => {
+ const pastDate = new Date(Date.now() - 100000);
+ beforeEach(() => {
+ factory({
+ propsData: {
+ model: {
+ ...environment,
+ auto_stop_at: pastDate,
+ },
+ canReadEnvironment: true,
+ tableData,
+ shouldShowAutoStopDate: true,
+ },
+ });
+ });
+
+ it('should not render a date', () => {
+ expect(findAutoStop().exists()).toBe(false);
+ });
+
+ it('should not render the suto-stop button', () => {
+ expect(wrapper.find(PinComponent).exists()).toBe(false);
+ });
+ });
+ });
});
describe('With manual actions', () => {
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
new file mode 100644
index 00000000000..d1d6735fa38
--- /dev/null
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/environments/event_hub';
+import PinComponent from '~/environments/components/environment_pin.vue';
+
+describe('Pin Component', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ // This destroys any wrappers created before a nested call to factory reassigns it
+ if (wrapper && wrapper.destroy) {
+ wrapper.destroy();
+ }
+ wrapper = shallowMount(PinComponent, {
+ ...options,
+ });
+ };
+
+ const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
+
+ beforeEach(() => {
+ factory({
+ propsData: {
+ autoStopUrl,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render the component with thumbtack icon', () => {
+ expect(wrapper.find(Icon).props('name')).toBe('thumbtack');
+ });
+
+ it('should emit onPinClick when clicked', () => {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const button = wrapper.find(GlButton);
+
+ button.vm.$emit('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index a014108b898..a2b581578d2 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -63,6 +63,7 @@ const environment = {
log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
+ auto_stop_at: null,
};
const folder = {
@@ -98,6 +99,10 @@ const tableData = {
title: 'Updated',
spacing: 'section-10',
},
+ autoStop: {
+ title: 'Auto stop in',
+ spacing: 'section-5',
+ },
actions: {
spacing: 'section-25',
},
diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
index aaea044595f..4c4359ad5d2 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false)
end
+ context 'when bridge job is passed in as parameter' do
+ let(:ci_config_path) { nil }
+ let(:bridge) { create(:ci_bridge) }
+
+ before do
+ command.bridge = bridge
+ end
+
+ context 'when bridge job has downstream yaml' do
+ before do
+ allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
+ end
+
+ it 'returns the content already available in command' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'bridge_source'
+ expect(command.config_content).to eq 'the-yaml'
+ end
+ end
+
+ context 'when bridge job does not have downstream yaml' do
+ before do
+ allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
+ end
+
+ it 'returns the next available source' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
+ expect(command.config_content).to eq(template.content)
+ end
+ end
+ end
+
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
+ context 'when bridge job is passed in as parameter' do
+ let(:ci_config_path) { nil }
+ let(:bridge) { create(:ci_bridge) }
+
+ before do
+ command.bridge = bridge
+ allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
+ end
+
+ it 'returns the content already available in command' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'bridge_source'
+ expect(command.config_content).to eq 'the-yaml'
+ end
+ end
+
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8ddb4c23b81..dc0851294b5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -6,6 +6,8 @@ issues:
- assignees
- updated_by
- milestone
+- issue_milestones
+- milestones
- notes
- resource_label_events
- resource_weight_events
@@ -78,6 +80,8 @@ milestone:
- boards
- milestone_releases
- releases
+- issue_milestones
+- merge_request_milestones
snippets:
- author
- project
@@ -106,6 +110,8 @@ merge_requests:
- assignee
- updated_by
- milestone
+- merge_request_milestones
+- milestones
- notes
- resource_label_events
- label_links
@@ -146,6 +152,12 @@ merge_requests:
- deployment_merge_requests
- deployments
- user_mentions
+issue_milestones:
+- milestone
+- issue
+merge_request_milestones:
+- milestone
+- merge_request
external_pull_requests:
- project
merge_request_diff:
@@ -189,6 +201,8 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
+- child_pipelines
+- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 6930f743c2f..bff3ac313c4 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -44,6 +44,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
+ describe '#public_id' do
+ it 'returns an encrypted, url-encoded session id' do
+ original_session_id = "!*'();:@&\n=+$,/?%abcd#123[4567]8"
+ active_session = ActiveSession.new(session_id: original_session_id)
+ encrypted_encoded_id = active_session.public_id
+
+ encrypted_id = CGI.unescape(encrypted_encoded_id)
+ derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
+
+ expect(original_session_id).to eq derived_session_id
+ end
+ end
+
describe '.list' do
it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis|
@@ -173,8 +186,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
device_name: 'iPhone 6',
device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'),
- updated_at: Time.zone.parse('2018-03-12 09:06'),
- session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
+ updated_at: Time.zone.parse('2018-03-12 09:06')
)
end
end
@@ -244,6 +256,40 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
+ describe '.destroy_with_public_id' do
+ it 'receives a user and public id and destroys the associated session' do
+ ActiveSession.set(user, request)
+ session = ActiveSession.list(user).first
+
+ ActiveSession.destroy_with_public_id(user, session.public_id)
+
+ total_sessions = ActiveSession.list(user).count
+ expect(total_sessions).to eq 0
+ end
+
+ it 'handles invalid input for public id' do
+ expect do
+ ActiveSession.destroy_with_public_id(user, nil)
+ end.not_to raise_error
+
+ expect do
+ ActiveSession.destroy_with_public_id(user, "")
+ end.not_to raise_error
+
+ expect do
+ ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
+ end.not_to raise_error
+ end
+
+ it 'does not attempt to destroy session when given invalid input for public id' do
+ expect(ActiveSession).not_to receive(:destroy)
+
+ ActiveSession.destroy_with_public_id(user, nil)
+ ActiveSession.destroy_with_public_id(user, "")
+ ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
+ end
+ end
+
describe '.cleanup' do
before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b30e88532e1..ce01765bb8c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#parent_pipeline' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when pipeline is triggered by a pipeline from the same project' do
+ let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: upstream_pipeline,
+ source_project: project,
+ pipeline: pipeline,
+ project: project)
+ end
+
+ it 'returns the parent pipeline' do
+ expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
+ end
+
+ it 'is child' do
+ expect(pipeline).to be_child
+ end
+ end
+
+ context 'when pipeline is triggered by a pipeline from another project' do
+ let(:upstream_pipeline) { create(:ci_pipeline) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: upstream_pipeline,
+ source_project: upstream_pipeline.project,
+ pipeline: pipeline,
+ project: project)
+ end
+
+ it 'returns nil' do
+ expect(pipeline.parent_pipeline).to be_nil
+ end
+
+ it 'is not child' do
+ expect(pipeline).not_to be_child
+ end
+ end
+
+ context 'when pipeline is not triggered by a pipeline' do
+ it 'returns nil' do
+ expect(pipeline.parent_pipeline).to be_nil
+ end
+
+ it 'is not child' do
+ expect(pipeline).not_to be_child
+ end
+ end
+ end
+
+ describe '#child_pipelines' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when pipeline triggered other pipelines on same project' do
+ let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ pipeline: downstream_pipeline,
+ project: pipeline.project)
+ end
+
+ it 'returns the child pipelines' do
+ expect(pipeline.child_pipelines).to eq [downstream_pipeline]
+ end
+
+ it 'is parent' do
+ expect(pipeline).to be_parent
+ end
+ end
+
+ context 'when pipeline triggered other pipelines on another project' do
+ let(:downstream_pipeline) { create(:ci_pipeline) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project)
+ end
+
+ it 'returns empty array' do
+ expect(pipeline.child_pipelines).to be_empty
+ end
+
+ it 'is not parent' do
+ expect(pipeline).not_to be_parent
+ end
+ end
+
+ context 'when pipeline did not trigger any pipelines' do
+ it 'returns empty array' do
+ expect(pipeline.child_pipelines).to be_empty
+ end
+
+ it 'is not parent' do
+ expect(pipeline).not_to be_parent
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 76a3a825978..2f4855efff0 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -53,43 +53,6 @@ describe Issuable do
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
-
- describe 'milestone' do
- let(:project) { create(:project) }
- let(:milestone_id) { create(:milestone, project: project).id }
- let(:params) do
- {
- title: 'something',
- project: project,
- author: build(:user),
- milestone_id: milestone_id
- }
- end
-
- subject { issuable_class.new(params) }
-
- context 'with correct params' do
- it { is_expected.to be_valid }
- end
-
- context 'with empty string milestone' do
- let(:milestone_id) { '' }
-
- it { is_expected.to be_valid }
- end
-
- context 'with nil milestone id' do
- let(:milestone_id) { nil }
-
- it { is_expected.to be_valid }
- end
-
- context 'with a milestone id from another project' do
- let(:milestone_id) { create(:milestone).id }
-
- it { is_expected.to be_invalid }
- end
- end
end
describe "Scope" do
@@ -141,48 +104,6 @@ describe Issuable do
end
end
- describe '#milestone_available?' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let(:issue) { create(:issue, project: project) }
-
- def build_issuable(milestone_id)
- issuable_class.new(project: project, milestone_id: milestone_id)
- end
-
- it 'returns true with a milestone from the issue project' do
- milestone = create(:milestone, project: project)
-
- expect(build_issuable(milestone.id).milestone_available?).to be_truthy
- end
-
- it 'returns true with a milestone from the issue project group' do
- milestone = create(:milestone, group: group)
-
- expect(build_issuable(milestone.id).milestone_available?).to be_truthy
- end
-
- it 'returns true with a milestone from the the parent of the issue project group' do
- parent = create(:group)
- group.update(parent: parent)
- milestone = create(:milestone, group: parent)
-
- expect(build_issuable(milestone.id).milestone_available?).to be_truthy
- end
-
- it 'returns false with a milestone from another project' do
- milestone = create(:milestone)
-
- expect(build_issuable(milestone.id).milestone_available?).to be_falsey
- end
-
- it 'returns false with a milestone from another group' do
- milestone = create(:milestone, group: create(:group))
-
- expect(build_issuable(milestone.id).milestone_available?).to be_falsey
- end
- end
-
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
@@ -809,27 +730,6 @@ describe Issuable do
end
end
- describe '#supports_milestone?' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
-
- context "for issues" do
- let(:issue) { build(:issue, project: project) }
-
- it 'returns true' do
- expect(issue.supports_milestone?).to be_truthy
- end
- end
-
- context "for merge requests" do
- let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
-
- it 'returns true' do
- expect(merge_request.supports_milestone?).to be_truthy
- end
- end
- end
-
describe '#matches_cross_reference_regex?' do
context "issue description with long path string" do
let(:mentionable) { build(:issue, description: "/a" * 50000) }
@@ -854,91 +754,4 @@ describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
-
- describe 'release scopes' do
- let_it_be(:project) { create(:project) }
- let(:forked_project) { fork_project(project) }
-
- let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
- let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
- let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
- let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
-
- let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
- let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
- let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
- let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
- let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
- let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
-
- let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
- let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
- let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
- let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
- let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
- let_it_be(:issue_6) { create(:issue, project: project) }
-
- let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
- let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
- let(:mr_3) { create(:merge_request, source_project: project) }
-
- let_it_be(:issue_items) { Issue.all }
- let(:mr_items) { MergeRequest.all }
-
- describe '#without_release' do
- it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
- expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
- expect(mr_items.without_release).to contain_exactly(mr_3)
- end
- end
-
- describe '#any_release' do
- it 'returns all issues or all mrs tied to a release' do
- expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
- expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
- end
- end
-
- describe '#with_release' do
- it 'returns the issues tied to a specfic release' do
- expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
- end
-
- it 'returns the mrs tied to a specific release' do
- expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
- end
-
- context 'when a release has a milestone with one issue and another one with no issue' do
- it 'returns that one issue' do
- expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
- end
-
- context 'when the milestone with no issue is added as a filter' do
- it 'returns an empty list' do
- expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
- end
- end
-
- context 'when the milestone with the issue is added as a filter' do
- it 'returns this issue' do
- expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
- end
- end
- end
-
- context 'when there is no issue or mr under a specific release' do
- it 'returns no issue or no mr' do
- expect(issue_items.with_release('v4.0', project.id)).to be_empty
- expect(mr_items.with_release('v4.0', project.id)).to be_empty
- end
- end
-
- context 'when a non-existent release tag is passed in' do
- it 'returns no issue or no mr' do
- expect(issue_items.with_release('v999.0', project.id)).to be_empty
- expect(mr_items.with_release('v999.0', project.id)).to be_empty
- end
- end
- end
- end
end
diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb
new file mode 100644
index 00000000000..186bf2c6290
--- /dev/null
+++ b/spec/models/concerns/milestoneable_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Milestoneable do
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+
+ shared_examples_for 'an object that can be assigned a milestone' do
+ describe 'Validation' do
+ describe 'milestone' do
+ let(:project) { create(:project, :repository) }
+ let(:milestone_id) { milestone.id }
+
+ subject { milestoneable_class.new(params) }
+
+ context 'with correct params' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'with empty string milestone' do
+ let(:milestone_id) { '' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with nil milestone id' do
+ let(:milestone_id) { nil }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with a milestone id from another project' do
+ let(:milestone_id) { create(:milestone).id }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when valid and saving' do
+ it 'copies the value to the new milestones relationship' do
+ subject.save!
+
+ expect(subject.milestones).to match_array([milestone])
+ end
+
+ context 'with old values in milestones relationship' do
+ let(:old_milestone) { create(:milestone, project: project) }
+
+ before do
+ subject.milestone = old_milestone
+ subject.save!
+ end
+
+ it 'replaces old values' do
+ expect(subject.milestones).to match_array([old_milestone])
+
+ subject.milestone = milestone
+ subject.save!
+
+ expect(subject.milestones).to match_array([milestone])
+ end
+
+ it 'can nullify the milestone' do
+ expect(subject.milestones).to match_array([old_milestone])
+
+ subject.milestone = nil
+ subject.save!
+
+ expect(subject.milestones).to match_array([])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#milestone_available?' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:issue) { create(:issue, project: project) }
+
+ def build_milestoneable(milestone_id)
+ milestoneable_class.new(project: project, milestone_id: milestone_id)
+ end
+
+ it 'returns true with a milestone from the issue project' do
+ milestone = create(:milestone, project: project)
+
+ expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns true with a milestone from the issue project group' do
+ milestone = create(:milestone, group: group)
+
+ expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns true with a milestone from the the parent of the issue project group' do
+ parent = create(:group)
+ group.update(parent: parent)
+ milestone = create(:milestone, group: parent)
+
+ expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns false with a milestone from another project' do
+ milestone = create(:milestone)
+
+ expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
+ end
+
+ it 'returns false with a milestone from another group' do
+ milestone = create(:milestone, group: create(:group))
+
+ expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
+ end
+ end
+ end
+
+ describe '#supports_milestone?' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ context "for issues" do
+ let(:issue) { build(:issue, project: project) }
+
+ it 'returns true' do
+ expect(issue.supports_milestone?).to be_truthy
+ end
+ end
+
+ context "for merge requests" do
+ let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
+
+ it 'returns true' do
+ expect(merge_request.supports_milestone?).to be_truthy
+ end
+ end
+ end
+
+ describe 'release scopes' do
+ let_it_be(:project) { create(:project) }
+
+ let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
+ let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
+ let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
+ let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
+
+ let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
+ let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
+ let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
+ let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
+ let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
+ let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
+
+ let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
+ let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
+ let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
+ let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
+ let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
+ let_it_be(:issue_6) { create(:issue, project: project) }
+
+ let_it_be(:items) { Issue.all }
+
+ describe '#without_release' do
+ it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
+ expect(items.without_release).to contain_exactly(issue_5, issue_6)
+ end
+ end
+
+ describe '#any_release' do
+ it 'returns all issues tied to a release' do
+ expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
+ end
+ end
+
+ describe '#with_release' do
+ it 'returns the issues tied a specfic release' do
+ expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
+ end
+
+ context 'when a release has a milestone with one issue and another one with no issue' do
+ it 'returns that one issue' do
+ expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
+ end
+
+ context 'when the milestone with no issue is added as a filter' do
+ it 'returns an empty list' do
+ expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
+ end
+ end
+
+ context 'when the milestone with the issue is added as a filter' do
+ it 'returns this issue' do
+ expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
+ end
+ end
+ end
+
+ context 'when there is no issue under a specific release' do
+ it 'returns no issue' do
+ expect(items.with_release('v4.0', project.id)).to be_empty
+ end
+ end
+
+ context 'when a non-existent release tag is passed in' do
+ it 'returns no issue' do
+ expect(items.with_release('v999.0', project.id)).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'Issues' do
+ let(:milestoneable_class) { Issue }
+ let(:params) do
+ {
+ title: 'something',
+ project: project,
+ author: user,
+ milestone_id: milestone_id
+ }
+ end
+
+ it_behaves_like 'an object that can be assigned a milestone'
+ end
+
+ context 'MergeRequests' do
+ let(:milestoneable_class) { MergeRequest }
+ let(:params) do
+ {
+ title: 'something',
+ source_project: project,
+ target_project: project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ author: user,
+ milestone_id: milestone_id
+ }
+ end
+
+ it_behaves_like 'an object that can be assigned a milestone'
+ end
+end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index e09c91e874a..bb88983e140 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -5,6 +5,12 @@ require 'spec_helper'
describe UserPreference do
let(:user_preference) { create(:user_preference) }
+ describe 'notes filters global keys' do
+ it 'contains expected values' do
+ expect(UserPreference::NOTES_FILTERS.keys).to match_array([:all_notes, :only_comments, :only_activity])
+ end
+ end
+
describe '#set_notes_filter' do
let(:issuable) { build_stubbed(:issue) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index cc2038a7245..b4416344ecf 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -101,6 +101,75 @@ describe API::Notes do
expect(json_response.first['body']).to eq(cross_reference_note.note)
end
end
+
+ context "activity filters" do
+ let!(:user_reference_note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "Hello there general!",
+ system: false
+ end
+
+ let(:test_url) {"/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes"}
+
+ shared_examples 'a notes request' do
+ it 'is a note array response' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context "when not provided" do
+ let(:count) { 2 }
+
+ before do
+ get api(test_url, private_user)
+ end
+
+ it_behaves_like 'a notes request'
+
+ it 'returns all the notes' do
+ expect(json_response.count).to eq(count)
+ end
+ end
+
+ context "when all_notes provided" do
+ let(:count) { 2 }
+
+ before do
+ get api(test_url + "?activity_filter=all_notes", private_user)
+ end
+
+ it_behaves_like 'a notes request'
+
+ it 'returns all the notes' do
+ expect(json_response.count).to eq(count)
+ end
+ end
+
+ context "when provided" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:filter, :count, :system_notable) do
+ "only_comments" | 1 | false
+ "only_activity" | 1 | true
+ end
+
+ with_them do
+ before do
+ get api(test_url + "?activity_filter=#{filter}", private_user)
+ end
+
+ it_behaves_like 'a notes request'
+
+ it "properly filters the returned notables" do
+ expect(json_response.count).to eq(count)
+ expect(json_response.first["system"]).to be system_notable
+ end
+ end
+ end
+ end
end
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
new file mode 100644
index 00000000000..33cd6e164b0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Ci::CreatePipelineService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+
+ context 'custom config content' do
+ let(:bridge) do
+ double(:bridge, yaml_for_downstream: <<~YML
+ rspec:
+ script: rspec
+ custom:
+ script: custom
+ YML
+ )
+ end
+
+ subject { service.execute(:push, bridge: bridge) }
+
+ it 'creates a pipeline using the content passed in as param' do
+ expect(subject).to be_persisted
+ expect(subject.builds.map(&:name)).to eq %w[rspec custom]
+ expect(subject.config_source).to eq 'bridge_source'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
new file mode 100644
index 00000000000..7dffbb04fdc
--- /dev/null
+++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+# Expects `worker_class` to be defined
+shared_examples_for 'reenqueuer' do
+ subject(:job) { worker_class.new }
+
+ before do
+ allow(job).to receive(:sleep) # faster tests
+ end
+
+ it 'implements lease_timeout' do
+ expect(job.lease_timeout).to be_a(ActiveSupport::Duration)
+ end
+
+ describe '#perform' do
+ it 'tries to obtain a lease' do
+ expect_to_obtain_exclusive_lease(job.lease_key)
+
+ job.perform
+ end
+ end
+end
+
+# Example usage:
+#
+# it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
+# subject { described_class.new }
+# let(:rate_limited_method) { subject.perform }
+# end
+#
+shared_examples_for 'it is rate limited to 1 call per' do |minimum_duration|
+ before do
+ # Allow Timecop freeze and travel without the block form
+ Timecop.safe_mode = false
+ Timecop.freeze
+
+ time_travel_during_rate_limited_method(actual_duration)
+ end
+
+ after do
+ Timecop.return
+ Timecop.safe_mode = true
+ end
+
+ context 'when the work finishes in 0 seconds' do
+ let(:actual_duration) { 0 }
+
+ it 'sleeps exactly the minimum duration' do
+ expect(subject).to receive(:sleep).with(a_value_within(0.01).of(minimum_duration))
+
+ rate_limited_method
+ end
+ end
+
+ context 'when the work finishes in 10% of minimum duration' do
+ let(:actual_duration) { 0.1 * minimum_duration }
+
+ it 'sleeps 90% of minimum duration' do
+ expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration))
+
+ rate_limited_method
+ end
+ end
+
+ context 'when the work finishes in 90% of minimum duration' do
+ let(:actual_duration) { 0.9 * minimum_duration }
+
+ it 'sleeps 10% of minimum duration' do
+ expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration))
+
+ rate_limited_method
+ end
+ end
+
+ context 'when the work finishes exactly at minimum duration' do
+ let(:actual_duration) { minimum_duration }
+
+ it 'does not sleep' do
+ expect(subject).not_to receive(:sleep)
+
+ rate_limited_method
+ end
+ end
+
+ context 'when the work takes 10% longer than minimum duration' do
+ let(:actual_duration) { 1.1 * minimum_duration }
+
+ it 'does not sleep' do
+ expect(subject).not_to receive(:sleep)
+
+ rate_limited_method
+ end
+ end
+
+ context 'when the work takes twice as long as minimum duration' do
+ let(:actual_duration) { 2 * minimum_duration }
+
+ it 'does not sleep' do
+ expect(subject).not_to receive(:sleep)
+
+ rate_limited_method
+ end
+ end
+
+ def time_travel_during_rate_limited_method(actual_duration)
+ # Save the original implementation of ensure_minimum_duration
+ original_ensure_minimum_duration = subject.method(:ensure_minimum_duration)
+
+ allow(subject).to receive(:ensure_minimum_duration) do |minimum_duration, &block|
+ original_ensure_minimum_duration.call(minimum_duration) do
+ # Time travel inside the block inside ensure_minimum_duration
+ Timecop.travel(actual_duration) if actual_duration && actual_duration > 0
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/reenqueuer_spec.rb b/spec/workers/concerns/reenqueuer_spec.rb
new file mode 100644
index 00000000000..b28f83d211b
--- /dev/null
+++ b/spec/workers/concerns/reenqueuer_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Reenqueuer do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:worker_class) do
+ Class.new do
+ def self.name
+ 'Gitlab::Foo::Bar::DummyWorker'
+ end
+
+ include ApplicationWorker
+ prepend Reenqueuer
+
+ attr_reader :performed_args
+
+ def perform(*args)
+ @performed_args = args
+
+ success? # for stubbing
+ end
+
+ def success?
+ false
+ end
+
+ def lease_timeout
+ 30.seconds
+ end
+ end
+ end
+
+ subject(:job) { worker_class.new }
+
+ before do
+ allow(job).to receive(:sleep) # faster tests
+ end
+
+ it_behaves_like 'reenqueuer'
+
+ it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
+ let(:rate_limited_method) { subject.perform }
+ end
+
+ it 'disables Sidekiq retries' do
+ expect(job.sidekiq_options_hash).to include('retry' => false)
+ end
+
+ describe '#perform', :clean_gitlab_redis_shared_state do
+ let(:arbitrary_args) { [:foo, 'bar', { a: 1 }] }
+
+ context 'when the lease is available' do
+ it 'does perform' do
+ job.perform(*arbitrary_args)
+
+ expect(job.performed_args).to eq(arbitrary_args)
+ end
+ end
+
+ context 'when the lease is taken' do
+ before do
+ stub_exclusive_lease_taken(job.lease_key)
+ end
+
+ it 'does not perform' do
+ job.perform(*arbitrary_args)
+
+ expect(job.performed_args).to be_nil
+ end
+ end
+
+ context 'when #perform returns truthy' do
+ before do
+ allow(job).to receive(:success?).and_return(true)
+ end
+
+ it 'reenqueues the worker' do
+ expect(worker_class).to receive(:perform_async)
+
+ job.perform
+ end
+ end
+
+ context 'when #perform returns falsey' do
+ it 'does not reenqueue the worker' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ job.perform
+ end
+ end
+ end
+end
+
+describe Reenqueuer::ReenqueuerSleeper do
+ let_it_be(:dummy_class) do
+ Class.new do
+ include Reenqueuer::ReenqueuerSleeper
+
+ def rate_limited_method
+ ensure_minimum_duration(11.seconds) do
+ # do work
+ end
+ end
+ end
+ end
+
+ subject(:dummy) { dummy_class.new }
+
+ # Test that rate_limited_method is rate limited by ensure_minimum_duration
+ it_behaves_like 'it is rate limited to 1 call per', 11.seconds do
+ let(:rate_limited_method) { dummy.rate_limited_method }
+ end
+
+ # Test ensure_minimum_duration more directly
+ describe '#ensure_minimum_duration' do
+ around do |example|
+ # Allow Timecop.travel without the block form
+ Timecop.safe_mode = false
+
+ Timecop.freeze do
+ example.run
+ end
+
+ Timecop.safe_mode = true
+ end
+
+ let(:minimum_duration) { 4.seconds }
+
+ context 'when the block completes well before the minimum duration' do
+ let(:time_left) { 3.seconds }
+
+ it 'sleeps until the minimum duration' do
+ expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
+
+ dummy.ensure_minimum_duration(minimum_duration) do
+ Timecop.travel(minimum_duration - time_left)
+ end
+ end
+ end
+
+ context 'when the block completes just before the minimum duration' do
+ let(:time_left) { 0.1.seconds }
+
+ it 'sleeps until the minimum duration' do
+ expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
+
+ dummy.ensure_minimum_duration(minimum_duration) do
+ Timecop.travel(minimum_duration - time_left)
+ end
+ end
+ end
+
+ context 'when the block completes just after the minimum duration' do
+ let(:time_over) { 0.1.seconds }
+
+ it 'does not sleep' do
+ expect(dummy).not_to receive(:sleep)
+
+ dummy.ensure_minimum_duration(minimum_duration) do
+ Timecop.travel(minimum_duration + time_over)
+ end
+ end
+ end
+
+ context 'when the block completes well after the minimum duration' do
+ let(:time_over) { 10.seconds }
+
+ it 'does not sleep' do
+ expect(dummy).not_to receive(:sleep)
+
+ dummy.ensure_minimum_duration(minimum_duration) do
+ Timecop.travel(minimum_duration + time_over)
+ end
+ end
+ end
+ end
+end