diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-03 12:07:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-03 12:07:33 +0300 |
commit | c0d8f9f3f962df6bfcc70440432da55d67307189 (patch) | |
tree | 457666705fbbd4f517d201680113406163829fcc /spec | |
parent | 2cfa1fc75dd4bd6d1f70d5fee1a824410694f297 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/features/profiles/active_sessions_spec.rb | 27 | ||||
-rw-r--r-- | spec/features/projects/environments/environment_spec.rb | 38 | ||||
-rw-r--r-- | spec/finders/pipelines_finder_spec.rb | 13 | ||||
-rw-r--r-- | spec/frontend/environments/environment_item_spec.js | 77 | ||||
-rw-r--r-- | spec/frontend/environments/environment_pin_spec.js | 46 | ||||
-rw-r--r-- | spec/frontend/environments/mock_data.js | 5 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb | 53 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/all_models.yml | 14 | ||||
-rw-r--r-- | spec/models/active_session_spec.rb | 50 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 110 | ||||
-rw-r--r-- | spec/models/concerns/issuable_spec.rb | 187 | ||||
-rw-r--r-- | spec/models/concerns/milestoneable_spec.rb | 243 | ||||
-rw-r--r-- | spec/models/user_preference_spec.rb | 6 | ||||
-rw-r--r-- | spec/requests/api/notes_spec.rb | 69 | ||||
-rw-r--r-- | spec/services/ci/create_pipeline_service/custom_config_content_spec.rb | 29 | ||||
-rw-r--r-- | spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb | 116 | ||||
-rw-r--r-- | spec/workers/concerns/reenqueuer_spec.rb | 179 |
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 |