diff options
author | Mike Greiling <mike@pixelcog.com> | 2017-09-18 10:56:04 -0500 |
---|---|---|
committer | Mike Greiling <mike@pixelcog.com> | 2017-09-18 10:56:04 -0500 |
commit | 57c97a10b0574a7fba14ef408ee0fbda1f96c646 (patch) | |
tree | cec79cb6506fe0bfa6347f29174794858ec57fad /spec | |
parent | 14a932f18094c2826cf958ff110075e32c18d684 (diff) | |
parent | 4cadf22e208e3be401824f43ab13d5e6f2ff6465 (diff) | |
download | gitlab-ce-57c97a10b0574a7fba14ef408ee0fbda1f96c646.tar.gz |
Merge branch 'master' into 37220-es-modules
* master: (148 commits)
Remove gaps under nav on build page
Replace the 'project/snippets.feature' spinach test with an rspec analog
Use correct group members path for members flyout link
Replace the 'project/commits/revert.feature' spinach test with an rspec analog
Merge branch 'rs-incoming-email-domain-docs' into 'security-10-0'
Replace the 'project/archived.feature' spinach test with an rspec analog
Fix broken link in docs/api/wiki.md
Fixed the new sidebars width when browser has scrollbars
Improve 'spec/features/profiles/*' specs
Replace the 'search.feature' spinach test with an rspec analog
dedupe yarn packages
add dependency approvals (all MIT license)
update build image to latest with node 8.x, yarn 1.0.2, and chrome 61
Ensure we use `Entities::User` for non-admin `users/:id` API requests
Minor update to address Sean McGivern's comment.
Add data migration
Fix setting share_with_group_lock
created services for keys
Prepare Repository#merge for migration to Gitaly
Never connect to webpack-dev-server over SSL
...
Diffstat (limited to 'spec')
112 files changed, 3943 insertions, 2246 deletions
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index dfa06c78d46..5163099cd98 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -45,6 +45,17 @@ describe Boards::IssuesController do expect(parsed_response.length).to eq 2 expect(development.issues.map(&:relative_position)).not_to include(nil) end + + it 'avoids N+1 database queries' do + create(:labeled_issue, project: project, labels: [development]) + control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count + + # 25 issues is bigger than the page size + # the relative position will ignore the `#make_sure_position_set` queries + create_list(:labeled_issue, 25, project: project, labels: [development], assignees: [johndoe], relative_position: 1) + + expect { list_issues(user: user, board: board, list: list2) }.not_to exceed_query_limit(control_count) + end end context 'with invalid list id' do diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index a5f544b4f92..a66b4ab0902 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,7 +25,8 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '1' ) patch :update, user: params, format: format @@ -40,7 +41,8 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '2' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(prefs) diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 745d051a5c1..5e0b57e9b2e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -367,5 +367,20 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end + + context 'when branch contains an invalid UTF-8 sequence' do + before do + project.repository.create_branch("wrong-\xE5-utf8-sequence") + end + + it 'return with a status 200' do + get :index, + namespace_id: project.namespace, + project_id: project, + format: :html + + expect(response).to have_http_status(200) + end + end end end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 2577d98df6f..7ce6a61d50c 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Group merge requests page' do end it 'ignores archived merge request count badges in navbar' do - expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1") + expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1") end it 'ignores archived merge request count badges in state-filters' do diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb new file mode 100644 index 00000000000..5ed4f3ad2bc --- /dev/null +++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'Groups > User sees users dropdowns in issuables list' do + let(:entity) { create(:group) } + let(:user_in_dropdown) { create(:user) } + let!(:user_not_in_dropdown) { create(:user) } + let!(:project) { create(:project, group: entity) } + + before do + entity.add_developer(user_in_dropdown) + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:issue, project: project) } + let(:issuables_path) { issues_group_path(entity) } + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:merge_request, source_project: project) } + let(:issuables_path) { merge_requests_group_path(entity) } + end +end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 9261acda9dc..7437c469a72 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -159,7 +159,7 @@ feature 'Issues > User uses quick actions', js: true do describe 'move the issue to another project' do let(:issue) { create(:issue, project: project) } - context 'when the project is valid', js: true do + context 'when the project is valid' do let(:target_project) { create(:project, :public) } before do @@ -180,7 +180,7 @@ feature 'Issues > User uses quick actions', js: true do end end - context 'when the project is valid but the user not authorized', js: true do + context 'when the project is valid but the user not authorized' do let(:project_unauthorized) {create(:project, :public)} before do @@ -196,7 +196,7 @@ feature 'Issues > User uses quick actions', js: true do end end - context 'when the project is invalid', js: true do + context 'when the project is invalid' do before do sign_in(user) visit project_issue_path(project, issue) @@ -210,7 +210,7 @@ feature 'Issues > User uses quick actions', js: true do end end - context 'when the user issues multiple commands', js: true do + context 'when the user issues multiple commands' do let(:target_project) { create(:project, :public) } let(:milestone) { create(:milestone, title: '1.0', project: project) } let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) } diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 624f13922ed..50c5e0bb65f 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -18,9 +18,9 @@ describe 'Milestone show' do it 'avoids N+1 database queries' do create(:labeled_issue, issue_params) - control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + control = ActiveRecord::QueryRecorder.new { visit_milestone } create_list(:labeled_issue, 10, issue_params) - expect { visit_milestone }.not_to exceed_query_limit(control_count) + expect { visit_milestone }.not_to exceed_query_limit(control) end end diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb new file mode 100644 index 00000000000..a8c08a680d7 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits the profile account page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_account_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Account') + end +end diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb new file mode 100644 index 00000000000..a50ebb29e01 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits the authentication log' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(audit_log_profile_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Authentication log') + end +end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index c935cdfd5c4..924ee0e4174 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -1,14 +1,19 @@ require 'spec_helper' -describe 'Profile > Preferences', :js do +describe 'User visits the profile preferences page' do let(:user) { create(:user) } before do sign_in(user) - visit profile_preferences_path + + visit(profile_preferences_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Preferences') end - describe 'User changes their syntax highlighting theme' do + describe 'User changes their syntax highlighting theme', :js do it 'creates a flash message' do choose 'user_color_scheme_id_5' @@ -27,7 +32,7 @@ describe 'Profile > Preferences', :js do end end - describe 'User changes their default dashboard' do + describe 'User changes their default dashboard', :js do it 'creates a flash message' do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb new file mode 100644 index 00000000000..6601d3039ed --- /dev/null +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits their profile' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Profile') + end +end diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb new file mode 100644 index 00000000000..685bf44619d --- /dev/null +++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits the profile SSH keys page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_keys_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('SSH Keys') + end +end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb new file mode 100644 index 00000000000..adff0a10f0e --- /dev/null +++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + page.all('.award-control .js-counter').each do |element| + expect(element).to have_content('0') + end + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_selector('gl-emoji[data-name="smile"]') + end +end diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb new file mode 100644 index 00000000000..221f1d7757e --- /dev/null +++ b/spec/features/projects/commit/user_reverts_commit_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'User reverts a commit', :js do + include RepoHelpers + + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(project_commit_path(project, sample_commit.id)) + + find('.header-action-buttons .dropdown').click + find('a[href="#modal-revert-commit"]').click + end + + context 'without creating a new merge request' do + before do + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + end + + it 'reverts a commit' do + expect(page).to have_content('The commit has been successfully reverted.') + end + + it 'does not revert a previously reverted commit' do + # Visit the comment again once it was reverted. + visit project_commit_path(project, sample_commit.id) + + find('.header-action-buttons .dropdown').click + find('a[href="#modal-revert-commit"]').click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('Sorry, we cannot revert this commit automatically.') + end + end + + context 'with creating a new merge request' do + it 'reverts a commit' do + page.within('#modal-revert-commit') do + click_button('Revert') + end + + expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") + end + end +end diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index bc102895aaf..a6f52c9ef58 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -108,6 +108,19 @@ feature 'Diff file viewer', :js do end end + context 'renamed file' do + before do + visit_commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') + end + + it 'shows the filename with diff highlight' do + within('.file-header-content') do + expect(page).to have_css('.idiff.left.right.deletion') + expect(page).to have_content('files/js/commit.coffee') + end + end + end + context 'binary file that appears to be text in the first 1024 bytes' do before do # The file we're visiting is smaller than 10 KB and we want it collapsed diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb new file mode 100644 index 00000000000..21c9acc7ac0 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'User browses a job', :js do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + build.success + build.trace.set('job trace') + + sign_in(user) + + visit(project_job_path(project, build)) + end + + it 'erases the job log' do + expect(page).to have_content("Job ##{build.id}") + expect(page).to have_css('#build-trace') + + click_link('Erase') + + expect(build).not_to have_trace + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + expect(page).to have_no_css('.artifacts') + + page.within('.erased') do + expect(page).to have_content('Job has been erased') + end + + expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all)) + end +end diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb new file mode 100644 index 00000000000..767777f3bf9 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'User browses jobs' do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/) + + sign_in(user) + + visit(project_jobs_path(project)) + end + + it 'shows the coverage' do + page.within('td.coverage') do + expect(page).to have_content('99.9%') + end + end + + it 'shows the "CI Lint" button' do + page.within('.nav-controls') do + ci_lint_tool_link = page.find_link('CI lint') + + expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) + end + end +end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 3b5c6966287..a4ed589f3de 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -164,9 +164,9 @@ feature 'Jobs' do end it 'links to issues/new with the title and description filled in' do - button_title = "Build Failed ##{job.id}" - job_path = project_job_path(project, job) - options = { issue: { title: button_title, description: job_path } } + button_title = "Job Failed ##{job.id}" + job_url = project_job_path(project, job) + options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } } href = new_project_issue_path(project, options) diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb new file mode 100644 index 00000000000..1bd2098af6d --- /dev/null +++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'User comments on a snippet', :js do + let(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_snippet_path(project, snippet)) + end + + it 'leaves a comment on a snippet' do + page.within('.js-main-target-form') do + fill_in('note_note', with: 'Good snippet!') + click_button('Comment') + end + + wait_for_requests + + expect(page).to have_content('Good snippet!') + end +end diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb new file mode 100644 index 00000000000..ca5f7981c33 --- /dev/null +++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'User deletes a snippet' do + let(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_snippet_path(project, snippet)) + end + + it 'deletes a snippet' do + first(:link, 'Delete').click + + expect(page).not_to have_content(snippet.title) + end +end diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb new file mode 100644 index 00000000000..09a390443cf --- /dev/null +++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'User updates a snippet' do + let(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_snippet_path(project, snippet)) + end + + it 'updates a snippet' do + page.within('.detail-page-header') do + first(:link, 'Edit').click + end + + fill_in('project_snippet_title', with: 'Snippet new title') + click_button('Save') + + expect(page).to have_content('Snippet new title') + end +end diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb new file mode 100644 index 00000000000..e9992e00ca8 --- /dev/null +++ b/spec/features/projects/snippets/user_views_snippets_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'User views snippets' do + let(:project) { create(:project) } + let!(:project_snippet) { create(:project_snippet, project: project, author: user) } + let!(:snippet) { create(:snippet, author: user) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_snippets_path(project)) + end + + it 'shows snippets' do + expect(page).to have_content(project_snippet.title) + expect(page).not_to have_content(snippet.title) + end +end diff --git a/spec/features/projects/user_archives_project_spec.rb b/spec/features/projects/user_archives_project_spec.rb new file mode 100644 index 00000000000..72063d13c2a --- /dev/null +++ b/spec/features/projects/user_archives_project_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'User archives a project' do + let(:user) { create(:user) } + + before do + project.add_master(user) + + sign_in(user) + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived, namespace: user.namespace) } + + before do + visit(edit_project_path(project)) + end + + it 'unarchives a project' do + expect(page).to have_content('Unarchive project') + + click_link('Unarchive') + + expect(page).not_to have_content('Archived project') + end + end + + context 'when a project is unarchived' do + let(:project) { create(:project, :repository, namespace: user.namespace) } + + before do + visit(edit_project_path(project)) + end + + it 'archives a project' do + expect(page).to have_content('Archive project') + + click_link('Archive') + + expect(page).to have_content('Archived') + end + end +end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb new file mode 100644 index 00000000000..0ed797a62ea --- /dev/null +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe 'User searches for code' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, namespace: user.namespace) } + + context 'when signed in' do + before do + project.add_master(user) + sign_in(user) + end + + it 'finds a file' do + visit(project_path(project)) + + page.within('.search') do + fill_in('search', with: 'application.js') + click_button('Go') + end + + click_link('Code') + + expect(page).to have_selector('.file-content .code') + expect(page).to have_selector("span.line[lang='javascript']") + end + + context 'when on a project page', :js do + before do + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds code' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: 'rspec') + find('.btn-search').trigger('click') + + page.within('.results') do + expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') + end + end + end + end + + context 'when signed out' do + let(:project) { create(:project, :public, :repository) } + + before do + visit(project_path(project)) + end + + it 'finds code' do + fill_in('search', with: 'rspec') + click_button('Go') + + page.within('.results') do + expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') + end + end + end +end diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb new file mode 100644 index 00000000000..c7c469a262c --- /dev/null +++ b/spec/features/search/user_searches_for_comments_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'User searches for comments' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(project_path(project)) + end + + context 'when a comment is in commits' do + context 'when comment belongs to an invalid commit' do + let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') } + + it 'finds a commit' do + page.within('.search') do + fill_in('search', with: comment.note) + click_button('Go') + end + + click_link('Comments') + + expect(page).to have_text('Commit deleted') + expect(page).to have_text('12345678') + end + end + end + + context 'when a comment is in a snippet' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') } + let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) } + + it 'finds a snippet' do + page.within('.search') do + fill_in('search', with: comment.note) + click_button('Go') + end + + click_link('Comments') + + expect(page).to have_link(snippet.title) + end + end +end diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb new file mode 100644 index 00000000000..28cae444588 --- /dev/null +++ b/spec/features/search/user_searches_for_commits_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'User searches for commits' do + let(:project) { create(:project, :repository) } + let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(search_path(project_id: project.id)) + end + + context 'when searching by SHA' do + it 'finds a commit and redirects to its page' do + fill_in('search', with: sha) + click_button('Search') + + expect(page).to have_current_path(project_commit_path(project, sha)) + end + + it 'finds a commit in uppercase and redirects to its page' do + fill_in('search', with: sha.upcase) + click_button('Search') + + expect(page).to have_current_path(project_commit_path(project, sha)) + end + end + + context 'when searching by message' do + it 'finds a commit and holds on /search page' do + create_commit('Message referencing another sha: "deadbeef"', project, user, 'master') + + fill_in('search', with: 'deadbeef') + click_button('Search') + + expect(page).to have_current_path('/search', only_path: true) + end + + it 'finds multiple commits' do + fill_in('search', with: 'See merge request') + click_button('Search') + click_link('Commits') + + expect(page).to have_selector('.commit-row-description', count: 9) + end + end +end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb new file mode 100644 index 00000000000..630a81b1c5e --- /dev/null +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe 'User searches for issues', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:issue1) { create(:issue, title: 'Foo', project: project) } + let!(:issue2) { create(:issue, title: 'Bar', project: project) } + + context 'when signed in' do + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds an issue' do + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + + context 'when on a project page' do + it 'finds an issue' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + end + end + + context 'when signed out' do + let(:project) { create(:project, :public) } + + before do + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds an issue' do + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb new file mode 100644 index 00000000000..116256682f4 --- /dev/null +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe 'User searches for merge requests', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) } + let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a merge request' do + fill_in('dashboard_search', with: merge_request1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Merge requests') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + end + end + + context 'when on a project page' do + it 'finds a merge request' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: merge_request1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Merge requests') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb new file mode 100644 index 00000000000..4fa9fe9ce8c --- /dev/null +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe 'User searches for milestones', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:milestone1) { create(:milestone, title: 'Foo', project: project) } + let!(:milestone2) { create(:milestone, title: 'Bar', project: project) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a milestone' do + fill_in('dashboard_search', with: milestone1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Milestones') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + end + end + + context 'when on a project page' do + it 'finds a milestone' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: milestone1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Milestones') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb new file mode 100644 index 00000000000..242e437e41c --- /dev/null +++ b/spec/features/search/user_searches_for_projects_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'User searches for projects' do + let!(:project) { create(:project, :public, name: 'Shop') } + + context 'when signed out' do + include_examples 'top right search form' + + it 'finds a project' do + visit(search_path) + + fill_in('dashboard_search', with: project.name[0..3]) + click_button('Search') + + expect(page).to have_link(project.name) + end + + it 'preserves the group being searched in' do + visit(search_path(group_id: project.namespace.id)) + + fill_in('search', with: 'foo') + click_button('Search') + + expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) + end + + it 'preserves the project being searched in' do + visit(search_path(project_id: project.id)) + + fill_in('search', with: 'foo') + click_button('Search') + + expect(find('#project_id', visible: false).value).to eq(project.id.to_s) + end + end +end diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb new file mode 100644 index 00000000000..1ea56479ecc --- /dev/null +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'User searches for wiki pages', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a page' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: 'content') + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Wiki') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(wiki_page.title) + end + end +end diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb new file mode 100644 index 00000000000..5ddea36add5 --- /dev/null +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe 'User uses header search field' do + include FilteredSearchHelpers + + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(project_path(project)) + end + + it 'starts searching by pressing the enter key', :js do + fill_in('search', with: 'gitlab') + find('#search').native.send_keys(:enter) + + page.within('.breadcrumbs-sub-title') do + expect(page).to have_content('Search') + end + end + + it 'contains location badge' do + expect(page).to have_selector('.has-location-badge') + end + + context 'when clicking the search field', :js do + before do + page.find('#search').click + end + + it 'shows category search dropdown' do + expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) + end + + context 'when clicking issues' do + let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } + + it 'shows assigned issues' do + find('.dropdown-menu').click_link('Issues assigned to me') + + expect(page).to have_selector('.filtered-search') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created issues' do + find('.dropdown-menu').click_link("Issues I've created") + + expect(page).to have_selector('.filtered-search') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + + context 'when clicking merge requests' do + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + + it 'shows assigned merge requests' do + find('.dropdown-menu').click_link('Merge requests assigned to me') + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created merge requests' do + find('.dropdown-menu').click_link("Merge requests I've created") + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + end + + context 'when entering text into the search field', :js do + before do + page.within('.search-input-wrap') do + fill_in('search', with: project.name[0..3]) + end + end + + it 'does not display the category search dropdown' do + expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + end + end +end diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb new file mode 100644 index 00000000000..95f3eb5e805 --- /dev/null +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'User uses search filters', :js do + let(:group) { create(:group) } + let!(:group_project) { create(:project, group: group) } + let(:project) { create(:project, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + group.add_owner(user) + sign_in(user) + + visit(search_path) + end + + context' when filtering by group' do + it 'shows group projects' do + find('.js-search-group-dropdown').trigger('click') + + wait_for_requests + + page.within('.search-holder') do + click_link(group.name) + end + + expect(find('.js-search-group-dropdown')).to have_content(group.name) + + page.within('.project-filter') do + find('.js-search-project-dropdown').trigger('click') + + wait_for_requests + + expect(page).to have_link(group_project.name_with_namespace) + end + end + end + + context' when filtering by project' do + it 'shows a project' do + page.within('.project-filter') do + find('.js-search-project-dropdown').trigger('click') + + wait_for_requests + + click_link(project.name_with_namespace) + end + + expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) + end + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb deleted file mode 100644 index 8f6d0bb9d1b..00000000000 --- a/spec/features/search_spec.rb +++ /dev/null @@ -1,310 +0,0 @@ -require 'spec_helper' - -describe "Search" do - include FilteredSearchHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - let!(:issue) { create(:issue, project: project, assignees: [user]) } - let!(:issue2) { create(:issue, project: project, author: user) } - - before do - sign_in(user) - project.team << [user, :reporter] - visit search_path - end - - it 'does not show top right search form' do - expect(page).not_to have_selector('.search') - end - - context 'search filters', js: true do - let(:group) { create(:group) } - let!(:group_project) { create(:project, group: group) } - - before do - group.add_owner(user) - end - - it 'shows group name after filtering' do - find('.js-search-group-dropdown').trigger('click') - wait_for_requests - - page.within '.search-holder' do - click_link group.name - end - - expect(find('.js-search-group-dropdown')).to have_content(group.name) - end - - it 'filters by group projects after filtering by group' do - find('.js-search-group-dropdown').trigger('click') - wait_for_requests - - page.within '.search-holder' do - click_link group.name - end - - expect(find('.js-search-group-dropdown')).to have_content(group.name) - - page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') - wait_for_requests - - expect(page).to have_link(group_project.name_with_namespace) - end - end - - it 'shows project name after filtering' do - page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') - wait_for_requests - - click_link project.name_with_namespace - end - - expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) - end - end - - describe 'searching for Projects' do - it 'finds a project' do - page.within '.search-holder' do - fill_in "search", with: project.name[0..3] - click_button "Search" - end - - expect(page).to have_content project.name - end - end - - context 'search for comments' do - context 'when comment belongs to a invalid commit' do - let(:project) { create(:project, :repository) } - let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } - - before do - note.update_attributes(commit_id: 12345678) - end - - it 'finds comment' do - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: note.note - click_button 'Go' - end - - click_link 'Comments' - - expect(page).to have_text("Commit deleted") - expect(page).to have_text("12345678") - end - end - - it 'finds a snippet' do - snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') - note = create(:note, - noteable: snippet, - author: user, - note: 'Supercalifragilisticexpialidocious', - project: project) - # Must visit project dashboard since global search won't search - # everything (e.g. comments, snippets, etc.) - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: note.note - click_button 'Go' - end - - click_link 'Comments' - - expect(page).to have_link(snippet.title) - end - - it 'finds a commit' do - project = create(:project, :repository) { |p| p.add_reporter(user) } - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: 'add' - click_button 'Go' - end - - click_link "Commits" - - expect(page).to have_selector('.commit-row-description') - end - - it 'finds a code' do - project = create(:project, :repository) { |p| p.add_reporter(user) } - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: 'application.js' - click_button 'Go' - end - - click_link "Code" - - expect(page).to have_selector('.file-content .code') - - expect(page).to have_selector("span.line[lang='javascript']") - end - end - - describe 'Right header search field' do - it 'allows enter key to search', js: true do - visit project_path(project) - fill_in 'search', with: 'gitlab' - find('#search').native.send_keys(:enter) - - page.within '.breadcrumbs-sub-title' do - expect(page).to have_content 'Search' - end - end - - describe 'Search in project page' do - before do - visit project_path(project) - end - - it 'shows top right search form' do - expect(page).to have_selector('#search') - end - - it 'contains location badge in top right search form' do - expect(page).to have_selector('.has-location-badge') - end - - context 'clicking the search field', js: true do - it 'shows category search dropdown' do - page.find('#search').click - - expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) - end - end - - context 'click the links in the category search dropdown', js: true do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } - - before do - page.find('#search').click - end - - it 'takes user to her issues page when issues assigned is clicked' do - find('.dropdown-menu').click_link 'Issues assigned to me' - - expect(page).to have_selector('.filtered-search') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her issues page when issues authored is clicked' do - find('.dropdown-menu').click_link "Issues I've created" - - expect(page).to have_selector('.filtered-search') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her MR page when MR assigned is clicked' do - find('.dropdown-menu').click_link 'Merge requests assigned to me' - - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her MR page when MR authored is clicked' do - find('.dropdown-menu').click_link "Merge requests I've created" - - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - end - - context 'entering text into the search field', js: true do - before do - page.within '.search-input-wrap' do - fill_in "search", with: project.name[0..3] - end - end - - it 'does not display the category search dropdown' do - expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) - end - end - end - end - - describe 'search for commits' do - let(:project) { create(:project, :repository) } - - before do - visit search_path(project_id: project.id) - end - - it 'redirects to commit page when search by sha and only commit found' do - fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f' - - click_button 'Search' - - expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) - end - - it 'redirects to single commit regardless of query case' do - fill_in 'search', with: '6D394385cf' - - click_button 'Search' - - expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) - end - - it 'holds on /search page when the only commit is found by message' do - create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master') - - fill_in 'search', with: 'deadbeef' - click_button 'Search' - - expect(page).to have_current_path('/search', only_path: true) - end - - it 'shows multiple matching commits' do - fill_in 'search', with: 'See merge request' - - click_button 'Search' - click_link 'Commits' - - expect(page).to have_selector('.commit-row-description', count: 9) - end - end - - context 'anonymous user' do - let(:project) { create(:project, :public) } - - before do - sign_out(user) - end - - it 'preserves the group being searched in' do - visit search_path(group_id: project.namespace.id) - - fill_in 'search', with: 'foo' - click_button 'Search' - - expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) - end - - it 'preserves the project being searched in' do - visit search_path(project_id: project.id) - - fill_in 'search', with: 'foo' - click_button 'Search' - - expect(find('#project_id', visible: false).value).to eq(project.id.to_s) - end - end -end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb new file mode 100644 index 00000000000..684af74d750 --- /dev/null +++ b/spec/finders/autocomplete_users_finder_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe AutocompleteUsersFinder do + describe '#execute' do + let!(:user1) { create(:user, username: 'johndoe') } + let!(:user2) { create(:user, :blocked, username: 'notsorandom') } + let!(:external_user) { create(:user, :external) } + let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } + let(:current_user) { create(:user) } + let(:params) { {} } + + let(:project) { nil } + let(:group) { nil } + + subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a } + + context 'when current_user not passed or nil' do + let(:current_user) { nil } + + it { is_expected.to match_array([]) } + end + + context 'when project passed' do + let(:project) { create(:project) } + + it { is_expected.to match_array([project.owner]) } + + context 'when author_id passed' do + let(:params) { { author_id: user2.id } } + + it { is_expected.to match_array([project.owner, user2]) } + end + end + + context 'when group passed and project not passed' do + let(:group) { create(:group, :public) } + + before do + group.add_users([user1], GroupMember::DEVELOPER) + end + + it { is_expected.to match_array([user1]) } + end + + it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) } + + context 'when filtered by search' do + let(:params) { { search: 'johndoe' } } + + it { is_expected.to match_array([user1]) } + end + + context 'when filtered by skip_users' do + let(:params) { { skip_users: [omniauth_user.id, current_user.id] } } + + it { is_expected.to match_array([user1, external_user]) } + end + + context 'when todos exist' do + let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) } + let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) } + let!(:done_todo1) { create(:todo, user: current_user, author: external_user, state: :done) } + let!(:done_todo2) { create(:todo, user: user1, author: external_user, state: :done) } + + context 'when filtered by todo_filter without todo_state_filter' do + let(:params) { { todo_filter: true } } + + it { is_expected.to match_array([]) } + end + + context 'when filtered by todo_filter with pending todo_state_filter' do + let(:params) { { todo_filter: true, todo_state_filter: 'pending' } } + + it { is_expected.to match_array([user1]) } + end + + context 'when filtered by todo_filter with done todo_state_filter' do + let(:params) { { todo_filter: true, todo_state_filter: 'done' } } + + it { is_expected.to match_array([external_user]) } + end + end + + context 'when filtered by current_user' do + let(:current_user) { user2 } + let(:params) { { current_user: true } } + + it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) } + end + + context 'when filtered by author_id' do + let(:params) { { author_id: user2.id } } + + it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) } + end + end +end diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index 6181b3ccc86..e6c1d9c9d84 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -19,6 +19,7 @@ "organization", "last_sign_in_at", "confirmed_at", + "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index b6d892548ef..5e272af6073 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -10,6 +10,8 @@ describe AutoDevopsHelper do before do allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed } allow(helper).to receive(:current_user) { user } + + Feature.get(:auto_devops_banner_disabled).disable end subject { helper.show_auto_devops_callout?(project) } @@ -18,6 +20,14 @@ describe AutoDevopsHelper do it { is_expected.to eq(true) } end + context 'when the banner is disabled by feature flag' do + it 'allows the feature flag to disable' do + Feature.get(:auto_devops_banner_disabled).enable + + expect(subject).to be(false) + end + end + context 'when dismissed' do before do helper.request.cookies[:auto_devops_settings_dismissed] = 'true' @@ -55,5 +65,21 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end + + context 'when master contains a .gitlab-ci.yml file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") + end + + it { is_expected.to eq(false) } + end + + context 'when another service is enabled' do + before do + create(:service, project: project, category: :ci, active: true) + end + + it { is_expected.to eq(false) } + end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 0deea0ff6a3..f9c31ac61d8 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -136,9 +136,9 @@ describe DiffHelper do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>}) - expect(marked_old_line).not_to be_html_safe + expect(marked_old_line).to be_html_safe expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>}) - expect(marked_new_line).not_to be_html_safe + expect(marked_new_line).to be_html_safe end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index a04c87b08eb..8b8080563d3 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PreferencesHelper do - describe 'dashboard_choices' do + describe '#dashboard_choices' do it 'raises an exception when defined choices may be missing' do expect(User).to receive(:dashboards).and_return(foo: 'foo') expect { helper.dashboard_choices }.to raise_error(RuntimeError) @@ -26,7 +26,33 @@ describe PreferencesHelper do end end - describe 'user_color_scheme' do + describe '#user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + stub_user(theme_id: 3) + + expect(helper.user_application_theme).to eq 'ui_light' + end + + it 'returns the default when id is invalid' do + stub_user(theme_id: Gitlab::Themes.count + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1) + + expect(helper.user_application_theme).to eq 'ui_indigo' + end + end + + context 'without a user' do + it 'returns the default theme' do + stub_user + + expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + + describe '#user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do allow(helper).to receive(:current_user) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 1437479831e..a76c75e0c08 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -469,4 +469,15 @@ describe ProjectsHelper do expect(recorder.count).to eq(1) end end + + describe '#git_user_name' do + let(:user) { double(:user, name: 'John "A" Doe53') } + before do + allow(helper).to receive(:current_user).and_return(user) + end + + it 'parses quotes in name' do + expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53') + end + end end diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb index 74bdbb01166..37cc08b3038 100644 --- a/spec/initializers/doorkeeper_spec.rb +++ b/spec/initializers/doorkeeper_spec.rb @@ -10,7 +10,7 @@ describe Doorkeeper.configuration do describe '#optional_scopes' do it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do - expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES + expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES - Gitlab::Auth::REGISTRY_SCOPES end end diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index b3c9bca64cc..02415485d19 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -10,6 +10,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ @@ -38,6 +39,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index f8b37c0edde..f4b4d7980a4 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -271,12 +271,19 @@ describe('Fly out sidebar navigation', () => { }); it('sets transform of sub-items', () => { + const sidebar = document.createElement('div'); const subItems = el.querySelector('.sidebar-sub-level-items'); + + sidebar.style.width = '200px'; + + document.body.appendChild(sidebar); + + setSidebar(sidebar); showSubLevelItems(el); expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); + ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb new file mode 100644 index 00000000000..6a11ca2f9d5 --- /dev/null +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Banzai::Pipeline::EmailPipeline do + describe '.filters' do + it 'returns the expected type' do + expect(described_class.filters).to be_kind_of(Banzai::FilterArray) + end + + it 'excludes ImageLazyLoadFilter' do + expect(described_class.filters).not_to be_empty + expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter) + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb deleted file mode 100644 index 1efd3113a43..00000000000 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ /dev/null @@ -1,1697 +0,0 @@ -require 'spec_helper' - -module Ci - describe GitlabCiYamlProcessor, :lib do - subject { described_class.new(config, path) } - let(:path) { 'path' } - - describe 'our current .gitlab-ci.yml' do - let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } - - it 'is valid' do - error_message = described_class.validation_message(config) - - expect(error_message).to be_nil - end - end - - describe '#build_attributes' do - subject { described_class.new(config, path).build_attributes(:rspec) } - - describe 'coverage entry' do - describe 'code coverage regexp' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - coverage: '/Code coverage: \d+\.\d+/' }) - end - - it 'includes coverage regexp in build attributes' do - expect(subject) - .to include(coverage_regex: 'Code coverage: \d+\.\d+') - end - end - end - - describe 'retry entry' do - context 'when retry count is specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', retry: 1 }) - end - - it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: 1) - end - end - - context 'when retry count is not specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'does not persist retry count in the database' do - expect(subject[:options]).not_to have_key(:retry) - end - end - end - - describe 'allow failure entry' do - context 'when job is a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual' }) - end - - it 'is allowed to fail' do - expect(subject[:allow_failure]).to be true - end - end - end - - context 'when job is not a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - end - end - end - - describe '#stage_seeds' do - context 'when no refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, - rspec: { stage: 'test', script: 'rspec' }, - spinach: { stage: 'test', script: 'spinach' }) - end - - let(:pipeline) { create(:ci_empty_pipeline) } - - it 'correctly fabricates a stage seeds object' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.second.stage[:name]).to eq 'deploy' - expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' - expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, ref: 'feature', tag: true) - end - - it 'returns stage seeds only assigned to master to master' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when source policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, - spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, source: :schedule) - end - - it 'returns stage seeds only assigned to schedules' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when kubernetes policy is specified' do - let(:pipeline) { create(:ci_empty_pipeline) } - - let(:config) do - YAML.dump( - spinach: { stage: 'test', script: 'spinach' }, - production: { - stage: 'deploy', - script: 'cap', - only: { kubernetes: 'active' } - } - ) - end - - context 'when kubernetes is active' do - let(:project) { create(:kubernetes_project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - - it 'returns seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when kubernetes is not active' do - it 'does not return seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - end - end - - describe "#builds_for_stage_and_ref" do - let(:type) { 'test' } - - it "returns builds if no branch specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec" } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - describe 'only' do - it "does not return builds if only has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if only has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "returns builds if only has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if only has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns builds if only has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if only has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if only has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if only has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns build only for specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", only: %w(master deploy) }, - staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, - production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:only) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:only) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:only) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - end - end - - describe 'except' do - it "returns builds if except has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if except has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "does not return builds if except has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if except has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "does not return builds if except has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if except has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if except has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if except has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns build except specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, - staging: { script: "deploy", type: "deploy", except: ["master"] }, - production: { script: "deploy", type: "deploy", except: ["master@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", except: except } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:except) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:except) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:except) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - end - end - end - - describe "Scripts handling" do - let(:config_data) { YAML.dump(config) } - let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) } - - subject { config_processor.builds_for_stage_and_ref("test", "master").first } - - describe "before_script" do - context "in global context" do - let(:config) do - { - before_script: ["global script"], - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("global script\nscript") - end - end - - context "overwritten in local context" do - let(:config) do - { - before_script: ["global script"], - test: { before_script: ["local script"], script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("local script\nscript") - end - end - end - - describe "script" do - let(:config) do - { - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("script") - end - end - - describe "after_script" do - context "in global context" do - let(:config) do - { - after_script: ["after_script"], - test: { script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["after_script"]) - end - end - - context "overwritten in local context" do - let(:config) do - { - after_script: ["local after_script"], - test: { after_script: ["local after_script"], script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["local after_script"]) - end - end - end - end - - describe "Image and service handling" do - context "when extended docker configuration is used" do - it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: ["mysql", { name: "docker:dind", alias: "docker", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "mysql" }, - { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, "docker:dind"], - script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, - { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - - context "when etended docker configuration is not used" do - it "returns image and service when defined" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql", "docker:dind"], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - end - - describe 'Variables' do - let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) } - - subject { config_processor.builds.first[:yaml_variables] } - - context 'when global variables are defined' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - variables: variables, - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns global variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job and global variables are defined' do - let(:global_variables) do - { 'VAR1' => 'global1', 'VAR3' => 'global3' } - end - let(:job_variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - before_script: ['pwd'], - variables: global_variables, - rspec: { script: 'rspec', variables: job_variables } - } - end - - it 'returns all unique variables' do - expect(subject).to contain_exactly( - { key: 'VAR3', value: 'global3', public: true }, - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job variables are defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec', variables: variables } - } - end - - context 'when syntax is correct' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - - it 'returns job variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when syntax is incorrect' do - context 'when variables defined but invalid' do - let(:variables) do - %w(VAR1 value1 VAR2 value2) - end - - it 'raises error' do - expect { subject } - .to raise_error(GitlabCiYamlProcessor::ValidationError, - /jobs:rspec:variables config should be a hash of key value pairs/) - end - end - - context 'when variables key defined but value not specified' do - let(:variables) do - nil - end - - it 'returns empty array' do - ## - # When variables config is empty, we assume this is a valid - # configuration, see issue #18775 - # - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - end - - context 'when job variables are not defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns empty array' do - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - - describe "When" do - %w(on_success on_failure always).each do |when_state| - it "returns #{when_state} when defined" do - config = YAML.dump({ - rspec: { script: "rspec", when: when_state } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:when]).to eq(when_state) - end - end - end - - describe 'cache' do - context 'when cache definition has unknown keys' do - it 'raises relevant validation error' do - config = YAML.dump( - { cache: { untracked: true, invalid: 'key' }, - rspec: { script: 'rspec' } }) - - expect { GitlabCiYamlProcessor.new(config) }.to raise_error( - GitlabCiYamlProcessor::ValidationError, - 'cache config contains unknown keys: invalid' - ) - end - end - - it "returns cache when defined globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - rspec: { - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "returns cache when defined in a job" do - config = YAML.dump({ - rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "overwrite cache when defined for a job and globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["test/"], - untracked: false, - key: 'local', - policy: 'pull-push' - ) - end - end - - describe "Artifacts" do - it "returns artifacts when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { - artifacts: { - paths: ["logs/", "binaries/"], - untracked: true, - name: "custom_name", - expire_in: "7d" - }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }], - artifacts: { - name: "custom_name", - paths: ["logs/", "binaries/"], - untracked: true, - expire_in: "7d" - } - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - - %w[on_success on_failure always].each do |when_state| - it "returns artifacts for when #{when_state} defined" do - config = YAML.dump({ - rspec: { - script: "rspec", - artifacts: { paths: ["logs/", "binaries/"], when: when_state } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:options][:artifacts][:when]).to eq(when_state) - end - end - end - - describe '#environment' do - let(:config) do - { - deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } - } - end - - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } - - context 'when a production environment is specified' do - let(:environment) { 'production' } - - it 'does return production' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment) - expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) - end - end - - context 'when hash is specified' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com' } - end - - it 'does return production and URL' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - - context 'the url has a port as variable' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com:$PORT' } - end - - it 'allows a variable for the port' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - end - end - - context 'when no environment is specified' do - let(:environment) { nil } - - it 'does return nil environment' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to be_nil - end - end - - context 'is not a string' do - let(:environment) { 1 } - - it 'raises error' do - expect { builds }.to raise_error( - 'jobs:deploy_to_production:environment config should be a hash or a string') - end - end - - context 'is not a valid string' do - let(:environment) { 'production:staging' } - - it 'raises error' do - expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") - end - end - - context 'when on_stop is specified' do - let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } - let(:config) { { review: review, close_review: close_review }.compact } - - context 'with matching job' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } - - it 'does return a list of builds' do - expect(builds.size).to eq(2) - expect(builds.first[:environment]).to eq('review') - end - end - - context 'without matching job' do - let(:close_review) { nil } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review is not defined') - end - end - - context 'with close job without environment' do - let(:close_review) { { stage: 'deploy', script: 'test' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') - end - end - - context 'with close job for different environment' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') - end - end - - context 'with close job without stop action' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') - end - end - end - end - - describe "Dependencies" do - let(:config) do - { - build1: { stage: 'build', script: 'test' }, - build2: { stage: 'build', script: 'test' }, - test1: { stage: 'test', script: 'test', dependencies: dependencies }, - test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } - } - end - - subject { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'no dependencies' do - let(:dependencies) { } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds' do - let(:dependencies) { %w(build1 build2) } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds defined as symbols' do - let(:dependencies) { [:build1, :build2] } - - it { expect { subject }.not_to raise_error } - end - - context 'undefined dependency' do - let(:dependencies) { ['undefined'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } - end - - context 'dependencies to deploy' do - let(:dependencies) { ['deploy'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } - end - end - - describe "Hidden jobs" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("test", "master") } - - shared_examples 'hidden_job_handling' do - it "doesn't create jobs that start with dot" do - expect(subject.size).to eq(1) - expect(subject.first).to eq({ - stage: "test", - stage_idx: 1, - name: "normal_job", - commands: "test", - coverage_regex: nil, - tag_list: [], - options: { - script: ["test"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when hidden job have a script definition' do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - - context "when hidden job doesn't have a script definition" do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - end - - describe "YAML Alias/Anchor" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("build", "master") } - - shared_examples 'job_templates_handling' do - it "is correctly supported for jobs" do - expect(subject.size).to eq(2) - expect(subject.first).to eq({ - stage: "build", - stage_idx: 0, - name: "job1", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - expect(subject.second).to eq({ - stage: "build", - stage_idx: 0, - name: "job2", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when template is a job' do - let(:config) do - <<EOT -job1: &JOBTMPL - stage: build - script: execute-script-for-job - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when template is a hidden job' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - script: execute-script-for-job - -job1: *JOBTMPL - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when job adds its own keys to a template definition' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - -job1: - <<: *JOBTMPL - script: execute-script-for-job - -job2: - <<: *JOBTMPL - script: execute-script-for-job -EOT - end - - it_behaves_like 'job_templates_handling' - end - end - - describe "Error handling" do - it "fails to parse YAML" do - expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) - end - - it "indicates that object is invalid" do - expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) - end - - it "returns errors if tags parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") - end - - it "returns errors if before_script parameter is invalid" do - config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings") - end - - it "returns errors if job before_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") - end - - it "returns errors if after_script parameter is invalid" do - config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings") - end - - it "returns errors if job after_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") - end - - it "returns errors if image parameter is invalid" do - config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string") - end - - it "returns errors if job name is blank" do - config = YAML.dump({ '' => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") - end - - it "returns errors if job name is non-string" do - config = YAML.dump({ 10 => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") - end - - it "returns errors if job image parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") - end - - it "returns errors if services parameter is not an array" do - config = YAML.dump({ services: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array") - end - - it "returns errors if services parameter is not an array of strings" do - config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns errors if job services parameter is not an array" do - config = YAML.dump({ rspec: { script: "test", services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array") - end - - it "returns errors if job services parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns error if job configuration is invalid" do - config = YAML.dump({ extra: "bundle update" }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") - end - - it "returns errors if services configuration is not correct" do - config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array") - end - - it "returns errors if there are no jobs defined" do - config = YAML.dump({ before_script: ["bundle update"] }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if there are no visible jobs defined" do - config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if job allow_failure parameter is not an boolean" do - config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") - end - - it "returns errors if job stage is not a string" do - config = YAML.dump({ rspec: { script: "test", type: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") - end - - it "returns errors if job stage is not a pre-defined stage" do - config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") - end - - it "returns errors if job stage is not a defined stage" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") - end - - it "returns errors if stages is not an array" do - config = YAML.dump({ stages: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if stages is not an array of strings" do - config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if variables is not a map" do - config = YAML.dump({ variables: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if variables is not a map of key-value strings" do - config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if job when is not on_success, on_failure or always" do - config = YAML.dump({ rspec: { script: "test", when: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") - end - - it "returns errors if job artifacts:name is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") - end - - it "returns errors if job artifacts:when is not an a predefined value" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") - end - - it "returns errors if job artifacts:expire_in is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:expire_in is not an a valid duration" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") - end - - it "returns errors if job artifacts:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") - end - - it "returns errors if cache:untracked is not an array of strings" do - config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value") - end - - it "returns errors if cache:paths is not an array of strings" do - config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings") - end - - it "returns errors if cache:key is not a string" do - config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol") - end - - it "returns errors if job cache:key is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") - end - - it "returns errors if job cache:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") - end - - it "returns errors if job cache:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") - end - - it "returns errors if job dependencies is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") - end - end - - describe "Validate configuration templates" do - templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") - - templates.each do |file| - it "does not return errors for #{file}" do - file = File.read(file) - - expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error - end - end - end - - describe "#validation_message" do - context "when the YAML could not be parsed" do - it "returns an error about invalid configutaion" do - content = YAML.dump("invalid: yaml: test") - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "Invalid configuration format" - end - end - - context "when the tags parameter is invalid" do - it "returns an error about invalid tags" do - content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "jobs:rspec tags should be an array of strings" - end - end - - context "when YAML content is empty" do - it "returns an error about missing content" do - expect(GitlabCiYamlProcessor.validation_message('')) - .to eq "Please provide content of .gitlab-ci.yml" - end - end - - context "when the YAML is valid" do - it "does not return any errors" do - content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - - expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil - end - end - end - end -end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f685bb83d0d..4f4a27e4c41 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -17,11 +17,31 @@ describe Gitlab::Auth do end it 'OPTIONAL_SCOPES contains all non-default scopes' do + stub_container_registry_config(enabled: true) + expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] end - it 'REGISTRY_SCOPES contains all registry related scopes' do - expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + context 'REGISTRY_SCOPES' do + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'is empty' do + expect(subject::REGISTRY_SCOPES).to eq [] + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'contains all registry related scopes' do + expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + end + end end end @@ -147,11 +167,17 @@ describe Gitlab::Auth do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end - it 'succeeds for personal access tokens with the `read_registry` scope' do - personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end end it 'succeeds if it is an impersonation token' do diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb new file mode 100644 index 00000000000..5c471cbdeda --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do + let!(:redirect_routes) { table(:redirect_routes) } + let!(:routes) { table(:routes) } + + before do + routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') + routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') + routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') + routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4') + routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5') + + # Valid redirects + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2') + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3') + + # Conflicting redirects + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') + end + + it 'deletes the conflicting redirect_routes in the range' do + expect(redirect_routes.count).to eq(8) + + expect do + described_class.new.perform(1, 3) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2) + + expect do + described_class.new.perform(4, 5) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0) + + expect(redirect_routes.count).to eq(3) + end +end diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index b155c20d8d3..cb52d971047 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -215,9 +215,17 @@ end # to a specific version of the database where said table is still present. # describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do + let(:user_class) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + end + end + let(:migration) { described_class.new } - let(:project) { create(:project_empty_repo) } - let(:author) { create(:user) } + let(:user_class) { table(:users) } + let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } + let(:namespace) { create(:namespace, owner: author) } + let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index e49ecadde20..e6645985ba4 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Ansi2html do +describe Gitlab::Ci::Ansi2html do subject { described_class } it "prints non-ansi as-is" do diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index f0769deef21..f8188675013 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Ci::Charts do +describe Gitlab::Ci::Charts do context "pipeline_times" do let(:project) { create(:project) } - let(:chart) { Ci::Charts::PipelineTime.new(project) } + let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } subject { chart.pipeline_times } diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb index f7b753b022b..3789a142248 100644 --- a/spec/lib/ci/mask_secret_spec.rb +++ b/spec/lib/gitlab/ci/mask_secret_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::MaskSecret do +describe Gitlab::Ci::MaskSecret do subject { described_class } describe '#mask' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb new file mode 100644 index 00000000000..2278230f338 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -0,0 +1,1699 @@ +require 'spec_helper' + +module Gitlab + module Ci + describe YamlProcessor, :lib do + subject { described_class.new(config, path) } + let(:path) { 'path' } + + describe 'our current .gitlab-ci.yml' do + let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } + + it 'is valid' do + error_message = described_class.validation_message(config) + + expect(error_message).to be_nil + end + end + + describe '#build_attributes' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + describe 'coverage entry' do + describe 'code coverage regexp' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + coverage: '/Code coverage: \d+\.\d+/' }) + end + + it 'includes coverage regexp in build attributes' do + expect(subject) + .to include(coverage_regex: 'Code coverage: \d+\.\d+') + end + end + end + + describe 'retry entry' do + context 'when retry count is specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', retry: 1 }) + end + + it 'includes retry count in build options attribute' do + expect(subject[:options]).to include(retry: 1) + end + end + + context 'when retry count is not specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'does not persist retry count in the database' do + expect(subject[:options]).not_to have_key(:retry) + end + end + end + + describe 'allow failure entry' do + context 'when job is a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual' }) + end + + it 'is allowed to fail' do + expect(subject[:allow_failure]).to be true + end + end + end + + context 'when job is not a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + end + end + end + + describe '#stage_seeds' do + context 'when no refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' }) + end + + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'correctly fabricates a stage seeds object' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.second.stage[:name]).to eq 'deploy' + expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' + expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'feature', tag: true) + end + + it 'returns stage seeds only assigned to master to master' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, source: :schedule) + end + + it 'returns stage seeds only assigned to schedules' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when kubernetes policy is specified' do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:config) do + YAML.dump( + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + ) + end + + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end + end + + describe "#builds_for_stage_and_ref" do + let(:type) { 'test' } + + it "returns builds if no branch specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + describe 'only' do + it "does not return builds if only has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if only has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "returns builds if only has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if only has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns builds if only has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if only has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end + end + + it "does not return builds if only has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end + end + + it "returns builds if only has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@path"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns build only for specified type" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: "test", only: %w(master deploy) }, + staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, + production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:only) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:only) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:only) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + end + end + + describe 'except' do + it "returns builds if except has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if except has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "does not return builds if except has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if except has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "does not return builds if except has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if except has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end + end + + it "returns builds if except has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end + end + + it "does not return builds if except has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@path"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns build except specified type" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, + staging: { script: "deploy", type: "deploy", except: ["master"] }, + production: { script: "deploy", type: "deploy", except: ["master@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", except: except } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:except) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:except) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:except) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + end + end + end + + describe "Scripts handling" do + let(:config_data) { YAML.dump(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) } + + subject { config_processor.builds_for_stage_and_ref("test", "master").first } + + describe "before_script" do + context "in global context" do + let(:config) do + { + before_script: ["global script"], + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("global script\nscript") + end + end + + context "overwritten in local context" do + let(:config) do + { + before_script: ["global script"], + test: { before_script: ["local script"], script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("local script\nscript") + end + end + end + + describe "script" do + let(:config) do + { + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("script") + end + end + + describe "after_script" do + context "in global context" do + let(:config) do + { + after_script: ["after_script"], + test: { script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["after_script"]) + end + end + + context "overwritten in local context" do + let(:config) do + { + after_script: ["local after_script"], + test: { after_script: ["local after_script"], script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["local after_script"]) + end + end + end + end + + describe "Image and service handling" do + context "when extended docker configuration is used" do + it "returns image and service when defined" do + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, + { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + + context "when etended docker configuration is not used" do + it "returns image and service when defined" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql", "docker:dind"], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + end + + describe 'Variables' do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) } + + subject { config_processor.builds.first[:yaml_variables] } + + context 'when global variables are defined' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + variables: variables, + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns global variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job and global variables are defined' do + let(:global_variables) do + { 'VAR1' => 'global1', 'VAR3' => 'global3' } + end + let(:job_variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + before_script: ['pwd'], + variables: global_variables, + rspec: { script: 'rspec', variables: job_variables } + } + end + + it 'returns all unique variables' do + expect(subject).to contain_exactly( + { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job variables are defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec', variables: variables } + } + end + + context 'when syntax is correct' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + + it 'returns job variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when syntax is incorrect' do + context 'when variables defined but invalid' do + let(:variables) do + %w(VAR1 value1 VAR2 value2) + end + + it 'raises error' do + expect { subject } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + /jobs:rspec:variables config should be a hash of key value pairs/) + end + end + + context 'when variables key defined but value not specified' do + let(:variables) do + nil + end + + it 'returns empty array' do + ## + # When variables config is empty, we assume this is a valid + # configuration, see issue #18775 + # + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + end + + context 'when job variables are not defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns empty array' do + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + + describe "When" do + %w(on_success on_failure always).each do |when_state| + it "returns #{when_state} when defined" do + config = YAML.dump({ + rspec: { script: "rspec", when: when_state } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + builds = config_processor.builds_for_stage_and_ref("test", "master") + expect(builds.size).to eq(1) + expect(builds.first[:when]).to eq(when_state) + end + end + end + + describe 'cache' do + context 'when cache definition has unknown keys' do + it 'raises relevant validation error' do + config = YAML.dump( + { cache: { untracked: true, invalid: 'key' }, + rspec: { script: 'rspec' } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + 'cache config contains unknown keys: invalid' + ) + end + end + + it "returns cache when defined globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + rspec: { + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "returns cache when defined in a job" do + config = YAML.dump({ + rspec: { + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "overwrite cache when defined for a job and globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["test/"], + untracked: false, + key: 'local', + policy: 'pull-push' + ) + end + end + + describe "Artifacts" do + it "returns artifacts when defined" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { + artifacts: { + paths: ["logs/", "binaries/"], + untracked: true, + name: "custom_name", + expire_in: "7d" + }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }], + artifacts: { + name: "custom_name", + paths: ["logs/", "binaries/"], + untracked: true, + expire_in: "7d" + } + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + + %w[on_success on_failure always].each do |when_state| + it "returns artifacts for when #{when_state} defined" do + config = YAML.dump({ + rspec: { + script: "rspec", + artifacts: { paths: ["logs/", "binaries/"], when: when_state } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + builds = config_processor.builds_for_stage_and_ref("test", "master") + expect(builds.size).to eq(1) + expect(builds.first[:options][:artifacts][:when]).to eq(when_state) + end + end + end + + describe '#environment' do + let(:config) do + { + deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } + } + end + + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } + + context 'when a production environment is specified' do + let(:environment) { 'production' } + + it 'does return production' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment) + expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) + end + end + + context 'when hash is specified' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com' } + end + + it 'does return production and URL' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end + end + + context 'when no environment is specified' do + let(:environment) { nil } + + it 'does return nil environment' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to be_nil + end + end + + context 'is not a string' do + let(:environment) { 1 } + + it 'raises error' do + expect { builds }.to raise_error( + 'jobs:deploy_to_production:environment config should be a hash or a string') + end + end + + context 'is not a valid string' do + let(:environment) { 'production:staging' } + + it 'raises error' do + expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") + end + end + + context 'when on_stop is specified' do + let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } + let(:config) { { review: review, close_review: close_review }.compact } + + context 'with matching job' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } + + it 'does return a list of builds' do + expect(builds.size).to eq(2) + expect(builds.first[:environment]).to eq('review') + end + end + + context 'without matching job' do + let(:close_review) { nil } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review is not defined') + end + end + + context 'with close job without environment' do + let(:close_review) { { stage: 'deploy', script: 'test' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') + end + end + + context 'with close job for different environment' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') + end + end + + context 'with close job without stop action' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') + end + end + end + end + + describe "Dependencies" do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + build2: { stage: 'build', script: 'test' }, + test1: { stage: 'test', script: 'test', dependencies: dependencies }, + test2: { stage: 'test', script: 'test' }, + deploy: { stage: 'test', script: 'test' } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'no dependencies' do + let(:dependencies) { } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds' do + let(:dependencies) { %w(build1 build2) } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds defined as symbols' do + let(:dependencies) { [:build1, :build2] } + + it { expect { subject }.not_to raise_error } + end + + context 'undefined dependency' do + let(:dependencies) { ['undefined'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } + end + + context 'dependencies to deploy' do + let(:dependencies) { ['deploy'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } + end + end + + describe "Hidden jobs" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("test", "master") } + + shared_examples 'hidden_job_handling' do + it "doesn't create jobs that start with dot" do + expect(subject.size).to eq(1) + expect(subject.first).to eq({ + stage: "test", + stage_idx: 1, + name: "normal_job", + commands: "test", + coverage_regex: nil, + tag_list: [], + options: { + script: ["test"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when hidden job have a script definition' do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + + context "when hidden job doesn't have a script definition" do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + end + + describe "YAML Alias/Anchor" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("build", "master") } + + shared_examples 'job_templates_handling' do + it "is correctly supported for jobs" do + expect(subject.size).to eq(2) + expect(subject.first).to eq({ + stage: "build", + stage_idx: 0, + name: "job1", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + expect(subject.second).to eq({ + stage: "build", + stage_idx: 0, + name: "job2", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when template is a job' do + let(:config) do + <<EOT +job1: &JOBTMPL + stage: build + script: execute-script-for-job + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when template is a hidden job' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + script: execute-script-for-job + +job1: *JOBTMPL + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when job adds its own keys to a template definition' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + +job1: + <<: *JOBTMPL + script: execute-script-for-job + +job2: + <<: *JOBTMPL + script: execute-script-for-job +EOT + end + + it_behaves_like 'job_templates_handling' + end + end + + describe "Error handling" do + it "fails to parse YAML" do + expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + end + + it "indicates that object is invalid" do + expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + + it "returns errors if tags parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") + end + + it "returns errors if before_script parameter is invalid" do + config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings") + end + + it "returns errors if job before_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") + end + + it "returns errors if after_script parameter is invalid" do + config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings") + end + + it "returns errors if job after_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") + end + + it "returns errors if image parameter is invalid" do + config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string") + end + + it "returns errors if job name is blank" do + config = YAML.dump({ '' => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank") + end + + it "returns errors if job name is non-string" do + config = YAML.dump({ 10 => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol") + end + + it "returns errors if job image parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") + end + + it "returns errors if services parameter is not an array" do + config = YAML.dump({ services: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array") + end + + it "returns errors if services parameter is not an array of strings" do + config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns errors if job services parameter is not an array" do + config = YAML.dump({ rspec: { script: "test", services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array") + end + + it "returns errors if job services parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns error if job configuration is invalid" do + config = YAML.dump({ extra: "bundle update" }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash") + end + + it "returns errors if services configuration is not correct" do + config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array") + end + + it "returns errors if there are no jobs defined" do + config = YAML.dump({ before_script: ["bundle update"] }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if job allow_failure parameter is not an boolean" do + config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") + end + + it "returns errors if job stage is not a string" do + config = YAML.dump({ rspec: { script: "test", type: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string") + end + + it "returns errors if job stage is not a pre-defined stage" do + config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end + + it "returns errors if job stage is not a defined stage" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end + + it "returns errors if stages is not an array" do + config = YAML.dump({ stages: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if stages is not an array of strings" do + config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if variables is not a map" do + config = YAML.dump({ variables: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if variables is not a map of key-value strings" do + config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if job when is not on_success, on_failure or always" do + config = YAML.dump({ rspec: { script: "test", when: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") + end + + it "returns errors if job artifacts:name is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") + end + + it "returns errors if job artifacts:when is not an a predefined value" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") + end + + it "returns errors if job artifacts:expire_in is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:expire_in is not an a valid duration" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") + end + + it "returns errors if job artifacts:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") + end + + it "returns errors if cache:untracked is not an array of strings" do + config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value") + end + + it "returns errors if cache:paths is not an array of strings" do + config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings") + end + + it "returns errors if cache:key is not a string" do + config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol") + end + + it "returns errors if job cache:key is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") + end + + it "returns errors if job cache:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") + end + + it "returns errors if job cache:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") + end + + it "returns errors if job dependencies is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") + end + end + + describe "Validate configuration templates" do + templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") + + templates.each do |file| + it "does not return errors for #{file}" do + file = File.read(file) + + expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error + end + end + end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(Gitlab::Ci::YamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cb430b47463..befdc18d1aa 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, spy, nil) } + expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 1bcdc369c44..3c8350b3aad 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do .to raise_error(RuntimeError, /Your database user is not allowed/) end end + + describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + before do + User.class_eval do + include EachBatch + end + end + + context 'with enough rows to bulk queue jobs more than once' do + before do + stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1) + end + + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in groups of buffer size 1' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]]) + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'with not enough rows to bulk queue jobs more than once' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in bulk all at once (big buffer size)' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]], + ['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'without specifying batch_size' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob') + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob') + end.to raise_error(StandardError, /does not have an ID/) + end + end + end + + describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + around do |example| + Timecop.freeze { example.run } + end + + before do + User.class_eval do + include EachBatch + end + end + + context 'with batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + end + end + end + + context 'without batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) + end.to raise_error(StandardError, /does not have an ID/) + end + end + end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 8beebc10040..4fa30d8df8b 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1761,17 +1761,9 @@ describe Gitlab::Diff::PositionTracer do let(:merge_commit) do update_file_again_commit - committer = repository.user_to_committer(current_user) - - options = { - message: "Merge branches", - author: committer, - committer: committer - } - merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") project.commit(branch_name) end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 14d64d8c4da..46e968cc398 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -401,7 +401,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '#stats' do + shared_examples '#stats' do subject { commit.stats } describe '#additions' do @@ -415,6 +415,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '#stats with gitaly on' do + it_should_behave_like '#stats' + end + + describe '#stats with gitaly disabled', skip_gitaly_mock: true do + it_should_behave_like '#stats' + end + describe '#to_diff' do subject { commit.to_diff } diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index e9c0209fe3b..d4d75b66659 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') } + let(:user) { Gitlab::Git::User.new('Jane Doe', 'janedoe@example.com', 'user-456') } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } @@ -18,7 +18,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(committer, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, @blankrev, @newrev, @ref) { } end end @@ -28,7 +28,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +40,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/user_spec.rb index b0ddbb51449..0ebcecb26c0 100644 --- a/spec/lib/gitlab/git/committer_spec.rb +++ b/spec/lib/gitlab/git/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::Committer do +describe Gitlab::Git::User do let(:name) { 'Jane Doe' } let(:email) { 'janedoe@example.com' } let(:gl_id) { 'user-123' } diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 4702a978f19..494dfe0e595 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::Git do @@ -29,4 +30,12 @@ describe Gitlab::Git do end end end + + describe '.ref_name' do + it 'ensure ref is a valid UTF-8 string' do + utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" + + expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_Ã¥") + end + end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index f32fe5d8150..ec3abcb0953 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -165,4 +165,29 @@ describe Gitlab::GitalyClient::CommitService do expect(subject).to eq("my diff") end end + + describe '#commit_stats' do + let(:request) do + Gitaly::CommitStatsRequest.new( + repository: repository_message, revision: revision + ) + end + let(:response) do + Gitaly::CommitStatsResponse.new( + oid: revision, + additions: 11, + deletions: 15 + ) + end + + subject { described_class.new(repository).commit_stats(revision) } + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats) + .with(request, kind_of(Hash)).and_return(response) + + expect(subject.additions).to eq(11) + expect(subject.deletions).to eq(15) + end + end end diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 08010c2d0e2..8dc83a6db7f 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#base_and_descendants' do @@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#all_groups' do @@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 8e3554375e8..d9b86e1bf34 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -119,7 +119,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do it 'has no when YML attributes but only the DB column' do allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) - expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes) + expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) saved_project_json end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index d5f4da3ce36..dbcc200b90b 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe Gitlab::OAuth::AuthHash do + let(:provider) { 'ldap'.freeze } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - provider: provider_ascii, + provider: provider, uid: uid_ascii, info: info_hash ) @@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" } - let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) } let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) } let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) } let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) } @@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) } let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) } - let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) } let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) } let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } @@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do end context 'defaults' do - it { expect(auth_hash.provider).to eql provider_utf8 } + it { expect(auth_hash.provider).to eq provider } it { expect(auth_hash.uid).to eql uid_utf8 } it { expect(auth_hash.email).to eql email_utf8 } it { expect(auth_hash.username).to eql nickname_utf8 } diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..ecacea6bb35 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_indigo') + expect(css).to include(' ui_dark ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Indigo' + expect(described_class.by_id(3).name).to eq 'Light' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index fdc3990132a..59c28431e1e 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -174,4 +174,13 @@ describe Gitlab::UrlSanitizer do end end end + + context 'when credentials contains special chars' do + it 'should parse the URL without errors' do + url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") + + expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") + expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git") + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 68429d792f2..c7d9f105f04 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -40,9 +40,13 @@ describe Gitlab::UsageData do ci_builds ci_internal_pipelines ci_external_pipelines + ci_pipeline_config_auto_devops + ci_pipeline_config_repository ci_runners ci_triggers ci_pipeline_schedules + auto_devops_enabled + auto_devops_disabled deploy_keys deployments environments diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb new file mode 100644 index 00000000000..2a61ff3ad65 --- /dev/null +++ b/spec/lib/system_check/orphans/namespace_check_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::NamespaceCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1 orphan2 second-level)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb new file mode 100644 index 00000000000..b0c2267d177 --- /dev/null +++ b/spec/lib/system_check/orphans/repository_check_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::RepositoryCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories) + # silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + let(:disk_repositories) { %w(repo1.git repo2.git) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb index 1396d12e5a9..759e77ac9db 100644 --- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:user_class) { table(:users) } + let(:settings_params) do [ { level: 0, events: [:new_note] }, # disabled, single event @@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = { user_id: user.id, level: params[:level], events: events } notification_setting = described_class::NotificationSetting.create(create_params) @@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = events.merge(user_id: user.id, level: params[:level]) notification_setting = described_class::NotificationSetting.create(create_params) diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb new file mode 100644 index 00000000000..1df2477da51 --- /dev/null +++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170907170235_delete_conflicting_redirect_routes') + +describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do + let!(:redirect_routes) { table(:redirect_routes) } + let!(:routes) { table(:routes) } + + around do |example| + Timecop.freeze { example.run } + end + + before do + stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2) + stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2) + + routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') + routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') + routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') + routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4') + routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5') + + # Valid redirects + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2') + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3') + + # Conflicting redirects + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) + expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + expect do + migrate! + end.to change { redirect_routes.count }.from(8).to(3) + end + end +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index d4da30b1641..f49a61062c1 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -1,8 +1,9 @@ require 'rails_helper' -RSpec.describe AbuseReport do - subject { create(:abuse_report) } - let(:user) { create(:admin) } +describe AbuseReport do + set(:report) { create(:abuse_report) } + set(:user) { create(:admin) } + subject { report } it { expect(subject).to be_valid } diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index b5d5d58697b..49f44525b29 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Appearance do +describe Appearance do subject { build(:appearance) } it { is_expected.to be_valid } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c7a9eabdf06..78cacf9ff5d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -167,19 +167,33 @@ describe ApplicationSetting do context 'housekeeping settings' do it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) } - it 'wants the full repack period to be longer than the incremental repack period' do + it 'wants the full repack period to be at least the incremental repack period' do subject.housekeeping_incremental_repack_period = 2 subject.housekeeping_full_repack_period = 1 expect(subject).not_to be_valid end - it 'wants the gc period to be longer than the full repack period' do - subject.housekeeping_full_repack_period = 2 - subject.housekeeping_gc_period = 1 + it 'wants the gc period to be at least the full repack period' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 90 expect(subject).not_to be_valid end + + it 'allows the same period for incremental repack and full repack, effectively skipping incremental repack' do + subject.housekeeping_incremental_repack_period = 2 + subject.housekeeping_full_repack_period = 2 + + expect(subject).to be_valid + end + + it 'allows the same period for full repack and gc, effectively skipping full repack' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 100 + + expect(subject).to be_valid + end end end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb index 8581bcbb08b..e89e534d914 100644 --- a/spec/models/chat_name_spec.rb +++ b/spec/models/chat_name_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe ChatName do - subject { create(:chat_name) } + set(:chat_name) { create(:chat_name) } + subject { chat_name } it { is_expected.to belong_to(:service) } it { is_expected.to belong_to(:user) } diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index e0e5f73e6fe..70a9a206faa 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe ChatTeam do - subject { create(:chat_team) } + set(:chat_team) { create(:chat_team) } + subject { chat_team } # Associations it { is_expected.to belong_to(:namespace) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c2c9f1c12d1..451968c7342 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,18 +1,19 @@ require 'spec_helper' describe Ci::Build do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:test_trace) { 'This is a test' } + set(:user) { create(:user) } + set(:group) { create(:group, :access_requestable) } + set(:project) { create(:project, :repository, group: group) } - let(:pipeline) do + set(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch, status: 'success') end + let(:build) { create(:ci_build, pipeline: pipeline) } + it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } @@ -282,7 +283,7 @@ describe Ci::Build do let(:project_regex) { '\(\d+\.\d+\) covered' } before do - project.build_coverage_regex = project_regex + project.update_column(:build_coverage_regex, project_regex) end context 'and coverage_regex attribute is not set' do @@ -1096,9 +1097,6 @@ describe Ci::Build do end describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - subject { build.repo_url } it { is_expected.to be_a(String) } @@ -1199,6 +1197,8 @@ describe Ci::Build do end context 'use from gitlab-ci.yml' do + let(:pipeline) { create(:ci_pipeline) } + before do stub_ci_pipeline_yaml_file(config) end @@ -1442,11 +1442,7 @@ describe Ci::Build do { key: 'SECRET_KEY', value: 'secret_value', public: false } end - let(:group) { create(:group, :access_requestable) } - before do - build.project.update(group: group) - create(:ci_group_variable, secret_variable.slice(:key, :value).merge(group: group)) end @@ -1459,11 +1455,7 @@ describe Ci::Build do { key: 'PROTECTED_KEY', value: 'protected_value', public: false } end - let(:group) { create(:group, :access_requestable) } - before do - build.project.update(group: group) - create(:ci_group_variable, :protected, protected_variable.slice(:key, :value).merge(group: group)) @@ -1486,6 +1478,10 @@ describe Ci::Build do end context 'when the ref is not protected' do + before do + build.update_column(:ref, 'some/feature') + end + it { is_expected.not_to include(protected_variable) } end end @@ -1552,6 +1548,8 @@ describe Ci::Build do end context 'when yaml_variables are undefined' do + let(:pipeline) { create(:ci_pipeline, project: project) } + before do build.yaml_variables = nil end @@ -1645,7 +1643,10 @@ describe Ci::Build do before do build.environment = 'production' - allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + + allow_any_instance_of(Project) + .to receive(:deployment_variables) + .and_return([deployment_variable]) end it { is_expected.to include(deployment_variable) } @@ -1669,14 +1670,19 @@ describe Ci::Build do before do allow(build).to receive(:predefined_variables) { [build_pre_var] } - allow(project).to receive(:predefined_variables) { [project_pre_var] } - allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] } allow(build).to receive(:yaml_variables) { [build_yaml_var] } - allow(project).to receive(:secret_variables_for) + allow_any_instance_of(Project) + .to receive(:predefined_variables) { [project_pre_var] } + + allow_any_instance_of(Project) + .to receive(:secret_variables_for) .with(ref: 'master', environment: nil) do [create(:ci_variable, key: 'secret', value: 'value')] end + + allow_any_instance_of(Ci::Pipeline) + .to receive(:predefined_variables) { [pipeline_pre_var] } end it do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 95da97b7bc5..77f0be6b120 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1439,4 +1439,24 @@ describe Ci::Pipeline, :mailer do it_behaves_like 'not sending any notification' end end + + describe '#latest_builds_with_artifacts' do + let!(:pipeline) { create(:ci_pipeline, :success) } + + let!(:build) do + create(:ci_build, :success, :artifacts, pipeline: pipeline) + end + + it 'returns the latest builds' do + expect(pipeline.latest_builds_with_artifacts).to eq([build]) + end + + it 'memoizes the returned relation' do + query_count = ActiveRecord::QueryRecorder + .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } } + .count + + expect(query_count).to eq(1) + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 11e64a0f877..e3cfa149e3a 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -207,11 +207,6 @@ eos context 'of a merge commit' do let(:repository) { project.repository } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test message', committer: author, author: author } - end - let(:merge_request) do create(:merge_request, source_branch: 'video', @@ -224,7 +219,7 @@ eos merge_commit_id = repository.merge(user, merge_request.diff_head_sha, merge_request, - commit_options) + 'Test message') repository.commit(merge_commit_id) end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 9c99c3e5c08..fadc8bfeb61 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -140,18 +140,6 @@ describe GpgKey do end end - describe 'notification', :mailer do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:gpg_key, user: user) - end - - should_email(user) - end - end - describe '#revoke' do it 'invalidates all associated gpg signatures and destroys the key' do gpg_key = create :gpg_key diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 96baeaff0a4..dbc4aba8547 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -169,16 +169,4 @@ describe Key, :mailer do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end end - - describe 'notification' do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:key, user: user) - end - - should_email(user) - end - end end diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb index d24d4cf7695..0a3180f43e8 100644 --- a/spec/models/lfs_objects_project_spec.rb +++ b/spec/models/lfs_objects_project_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe LfsObjectsProject do - subject { create(:lfs_objects_project, project: project) } - let(:project) { create(:project) } + set(:project) { create(:project) } + + subject do + create(:lfs_objects_project, project: project) + end describe 'associations' do it { is_expected.to belong_to(:project) } @@ -11,9 +14,13 @@ describe LfsObjectsProject do describe 'validation' do it { is_expected.to validate_presence_of(:lfs_object_id) } - it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") } - it { is_expected.to validate_presence_of(:project_id) } + + it 'validates object id' do + is_expected.to validate_uniqueness_of(:lfs_object_id) + .scoped_to(:project_id) + .with_message("already exists in project") + end end describe '#update_project_statistics' do diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index b2f2a3ce914..01440b15674 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -41,7 +41,7 @@ describe PersonalAccessToken do it 'revokes the token' do active_personal_access_token.revoke! - expect(active_personal_access_token.revoked?).to be true + expect(active_personal_access_token).to be_revoked end end @@ -61,10 +61,37 @@ describe PersonalAccessToken do expect(personal_access_token).to be_valid end - it "allows creating a token with read_registry scope" do - personal_access_token.scopes = [:read_registry] + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end - expect(personal_access_token).to be_valid + it "rejects creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).not_to be_valid + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" + end + + it "allows revoking a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + personal_access_token.revoke! + + expect(personal_access_token).to be_revoked + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end end it "rejects creating a token with unavailable scopes" do diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 5faab9ba38b..be07ca2d945 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -6,7 +6,8 @@ describe PipelinesEmailService, :mailer do end let(:project) { create(:project, :repository) } - let(:recipient) { 'test@gitlab.com' } + let(:recipients) { 'test@gitlab.com' } + let(:receivers) { [recipients] } let(:data) do Gitlab::DataBuilder::Pipeline.build(pipeline) @@ -48,18 +49,24 @@ describe PipelinesEmailService, :mailer do shared_examples 'sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end end it 'sends email' do - should_only_email(double(notification_email: recipient), kind: :bcc) + emails = receivers.map { |r| double(notification_email: r) } + + should_only_email(*emails, kind: :bcc) end end shared_examples 'not sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end @@ -75,10 +82,6 @@ describe PipelinesEmailService, :mailer do subject.test(data) end - before do - subject.recipients = recipient - end - context 'when pipeline is failed' do before do data[:object_attributes][:status] = 'failed' @@ -104,10 +107,6 @@ describe PipelinesEmailService, :mailer do end context 'with recipients' do - before do - subject.recipients = recipient - end - context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' @@ -152,9 +151,7 @@ describe PipelinesEmailService, :mailer do end context 'with empty recipients list' do - before do - subject.recipients = ' ,, ' - end + let(:recipients) { ' ,, ' } context 'with failed pipeline' do before do @@ -165,5 +162,19 @@ describe PipelinesEmailService, :mailer do it_behaves_like 'not sending email' end end + + context 'with recipients list separating with newlines' do + let(:recipients) { "\ntest@gitlab.com, \r\nexample@gitlab.com" } + let(:receivers) { %w[test@gitlab.com example@gitlab.com] } + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48fc77423ff..78226c6c3fa 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2682,4 +2682,60 @@ describe Project do end end end + + describe '#latest_successful_builds_for' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + context 'without a ref' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for + end + end + + context 'with the ref set to the default branch' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for(project.default_branch) + end + end + + context 'with a ref that is not the default branch' do + it 'returns the latest successful pipeline for the given ref' do + expect(project.pipelines).to receive(:latest_successful_for).with('foo') + + project.latest_successful_pipeline_for('foo') + end + end + end + + describe '#latest_successful_pipeline_for_default_branch' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + it 'memoizes and returns the latest successful pipeline for the default branch' do + pipeline = double(:pipeline) + + expect(project.pipelines).to receive(:latest_successful_for) + .with(project.default_branch) + .and_return(pipeline) + .once + + 2.times do + expect(project.latest_successful_pipeline_for_default_branch) + .to eq(pipeline) + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7065d467ec0..60cd7e70055 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -8,12 +8,9 @@ describe Repository, models: true do let(:repository) { project.repository } let(:broken_repository) { create(:project, :broken_storage).repository } let(:user) { create(:user) } - let(:committer) { Gitlab::Git::Committer.from_user(user) } + let(:git_user) { Gitlab::Git::User.from_gitlab(user) } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test message', committer: author, author: author } - end + let(:message) { 'Test message' } let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) @@ -21,7 +18,7 @@ describe Repository, models: true do merge_commit_id = repository.merge(user, merge_request.diff_head_sha, merge_request, - commit_options) + message) repository.commit(merge_commit_id) end @@ -886,7 +883,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -932,20 +929,20 @@ describe Repository, models: true do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref) + .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end it 'runs without errors' do expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - service = Gitlab::Git::OperationService.new(committer, repository.raw_repository) + service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository) expect(service).to receive(:update_autocrlf_option) @@ -956,7 +953,7 @@ describe Repository, models: true do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end @@ -974,7 +971,7 @@ describe Repository, models: true do expect(target_project.repository.raw_repository).to receive(:fetch_ref) .and_call_original - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch( 'master', start_repository: project.repository.raw_repository, @@ -990,7 +987,7 @@ describe Repository, models: true do it 'does not fetch_ref and just pass the commit' do expect(target_repository).not_to receive(:fetch_ref) - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } end end @@ -1009,7 +1006,7 @@ describe Repository, models: true do end expect do - Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository, &:itself) @@ -1031,7 +1028,7 @@ describe Repository, models: true do repository.add_branch(user, branch, old_rev) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.not_to raise_error @@ -1049,7 +1046,7 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.to raise_error(Gitlab::Git::CommitError) @@ -1061,7 +1058,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) @@ -1287,10 +1284,7 @@ describe Repository, models: true do describe '#merge' do let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test \r\n\r\n message', committer: author, author: author } - end + let(:message) { 'Test \r\n\r\n message' } it 'merges the code and returns the commit id' do expect(merge_commit).to be_present @@ -1298,19 +1292,19 @@ describe Repository, models: true do end it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do - merge_commit_id = merge(repository, user, merge_request, commit_options) + merge_commit_id = merge(repository, user, merge_request, message) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end it 'removes carriage returns from commit message' do - merge_commit_id = merge(repository, user, merge_request, commit_options) + merge_commit_id = merge(repository, user, merge_request, message) - expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r")) + expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) end - def merge(repository, user, merge_request, options = {}) - repository.merge(user, merge_request.diff_head_sha, merge_request, options) + def merge(repository, user, merge_request, message) + repository.merge(user, merge_request.diff_head_sha, merge_request, message) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3ba01313efb..c1affa812aa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -716,6 +716,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end @@ -726,6 +727,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9602584f546..92e7d797cbd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -54,9 +54,9 @@ describe API::Projects do shared_examples_for 'projects response without N + 1 queries' do it 'avoids N + 1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + control = ActiveRecord::QueryRecorder.new do get api('/projects', current_user) - end.count + end if defined?(additional_project) additional_project @@ -66,7 +66,7 @@ describe API::Projects do expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control_count + 8) + end.not_to exceed_query_limit(control).with_threshold(8) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 48d99841385..7e174903918 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -1,10 +1,13 @@ require "spec_helper" describe API::Services do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:user2) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } + set(:user2) { create(:user) } + + set(:project) do + create(:project, creator_id: user.id, namespace: user.namespace) + end Service.available_services_names.each do |service| describe "PUT /projects/:id/services/#{service.dasherize}" do @@ -98,8 +101,6 @@ describe API::Services do end describe 'POST /projects/:id/services/:slug/trigger' do - let!(:project) { create(:project) } - describe 'Mattermost Service' do let(:service_name) { 'mattermost_slash_commands' } diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 8d79ea3dd40..41bf43a9bce 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -49,6 +49,10 @@ describe JwtController do let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + before do + stub_container_registry_config(enabled: true) + end + subject! { get '/jwt/auth', parameters, headers } it 'authenticates correctly' do diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb index 9a6875e448c..f4ff818c479 100644 --- a/spec/services/ci/pipeline_trigger_service_spec.rb +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -34,6 +34,8 @@ describe Ci::PipelineTriggerService do expect(result[:pipeline].ref).to eq('master') expect(result[:pipeline].project).to eq(project) expect(result[:pipeline].user).to eq(trigger.owner) + expect(result[:pipeline].trigger_requests.to_a) + .to eq(result[:pipeline].builds.map(&:trigger_request).uniq) expect(result[:status]).to eq(:success) end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index bbc3a8c79f5..fbb3213f42b 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' describe Ci::RetryBuildService do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + set(:user) { create(:user) } + set(:project) { create(:project) } + set(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:service) do @@ -37,7 +38,7 @@ describe Ci::RetryBuildService do :queued, :coverage, :tags, :allowed_to_fail, :on_tag, :triggered, :trace, :teardown_environment, description: 'my-job', stage: 'test', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline)) do |build| + auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build| ## # TODO, workaround for FactoryGirl limitation when having both # stage (text) and stage_id (integer) columns in the table. diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb new file mode 100644 index 00000000000..7a604c0cadd --- /dev/null +++ b/spec/services/deploy_keys/create_service_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe DeployKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:deploy_key) } + + subject { described_class.new(user, params) } + + it "creates a deploy key" do + expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1) + end +end diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb new file mode 100644 index 00000000000..20382a3a618 --- /dev/null +++ b/spec/services/gpg_keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe GpgKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:gpg_key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a gpg key' do + expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1) + end +end diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb new file mode 100644 index 00000000000..bcb436c1e46 --- /dev/null +++ b/spec/services/keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Keys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a key' do + expect { subject.execute }.to change { user.keys.where(params).count }.by(1) + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2af2485eeed..64e676f22a0 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -150,9 +150,7 @@ describe MergeRequests::RefreshService do context 'manual merge of source branch' do before do # Merge master -> feature branch - author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } - commit_options = { message: 'Test message', committer: author, author: author } - @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options) + @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message') commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 3e493148b32..f4b36eb7eeb 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -84,7 +84,6 @@ describe NotificationService, :mailer do let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } - it { should_email(key.user) } describe 'never emails the ghost user' do let(:key_options) { { user: User.ghost } } diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 437c009e7fa..b7b5de07380 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -75,7 +75,7 @@ describe Projects::HousekeepingService do end end - it 'uses all three kinds of housekeeping we offer' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) allow(subject).to receive(:lease_key).and_return(:the_lease_key) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 92cc9a37795..c551083ac90 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -57,6 +57,21 @@ describe Projects::UpdateService, '#execute' do end end end + + context 'When project visibility is higher than parent group' do + let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + before do + project.update(namespace: group, visibility_level: group.visibility_level) + end + + it 'does not update project visibility level' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) + expect(project.reload).to be_internal + end + end end describe 'when updating project that has forks' do @@ -148,7 +163,7 @@ describe Projects::UpdateService, '#execute' do result = update_project(project, admin, path: 'existing') expect(result).to include(status: :error) - expect(result[:message]).to match('Project could not be updated!') + expect(result[:message]).to match('There is already a repository with that name on disk') expect(project).not_to be_valid expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') @@ -159,8 +174,10 @@ describe Projects::UpdateService, '#execute' do it 'returns an error result when record cannot be updated' do result = update_project(project, admin, { name: 'foo&bar' }) - expect(result).to eq({ status: :error, - message: 'Project could not be updated!' }) + expect(result).to eq({ + status: :error, + message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." + }) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ff1754fbe7e..92735336572 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ require './spec/simplecov_env' SimpleCovEnv.start! -ENV["RAILS_ENV"] ||= 'test' +ENV["RAILS_ENV"] = 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' require File.expand_path("../../config/environment", __FILE__) diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index 3bd589d64b9..57e28e040d7 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -23,6 +23,10 @@ shared_examples_for 'allows the "read_user" scope' do context 'when the requesting token does not have any required scope' do let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + before do + stub_container_registry_config(enabled: true) + end + it 'returns a "401" response' do get api_call.call(path, user, personal_access_token: token) diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index cd55d63125e..688175369ae 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index cd55d63125e..ce8dfe5ae75 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} +}
\ No newline at end of file diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb new file mode 100644 index 00000000000..5b6d9c1a4df --- /dev/null +++ b/spec/support/matchers/navigation_matcher.rb @@ -0,0 +1,6 @@ +RSpec::Matchers.define :have_active_navigation do |expected| + match do |page| + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected) + end +end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb index 55b531b4cf7..ba0b805caad 100644 --- a/spec/support/query_recorder.rb +++ b/spec/support/query_recorder.rb @@ -34,15 +34,47 @@ RSpec::Matchers.define :exceed_query_limit do |expected| supports_block_expectations match do |block| - query_count(&block) > expected + query_count(&block) > expected_count + threshold end failure_message_when_negated do |actual| - "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + threshold_message = threshold > 0 ? " (+#{@threshold})" : '' + counts = "#{expected_count}#{threshold_message}" + "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" + end + + def with_threshold(threshold) + @threshold = threshold + self + end + + def threshold + @threshold.to_i + end + + def expected_count + if expected.is_a?(ActiveRecord::QueryRecorder) + expected.count + else + expected + end + end + + def actual_count + @recorder.count end def query_count(&block) @recorder = ActiveRecord::QueryRecorder.new(&block) @recorder.count end + + def log_message + if expected.is_a?(ActiveRecord::QueryRecorder) + extra_queries = (expected.log - @recorder.log).join("\n\n") + "Extra queries: \n\n #{extra_queries}" + else + @recorder.log_message + end + end end diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb new file mode 100644 index 00000000000..c92c7f603d6 --- /dev/null +++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb @@ -0,0 +1,21 @@ +shared_examples 'issuable user dropdown behaviors' do + include FilteredSearchHelpers + + before do + issuable # ensure we have at least one issuable + sign_in(user_in_dropdown) + end + + %w[author assignee].each do |dropdown| + describe "#{dropdown} dropdown", :js do + it 'only includes members of the project/group' do + visit issuables_path + + filtered_search.set("#{dropdown}:") + + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) + end + end + end +end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index 639b0924197..639b0924197 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb diff --git a/spec/support/shared_examples/features/search_shared_examples.rb b/spec/support/shared_examples/features/search_shared_examples.rb new file mode 100644 index 00000000000..25ebbf011d5 --- /dev/null +++ b/spec/support/shared_examples/features/search_shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples 'top right search form' do + it 'does not show top right search form' do + expect(page).not_to have_selector('.search') + end +end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..9695f35bd25 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -26,9 +26,11 @@ module StubGitlabCalls end def stub_container_registry_config(registry_settings) - allow(Gitlab.config.registry).to receive_messages(registry_settings) allow(Auth::ContainerRegistryAuthenticationService) .to receive(:full_access_token).and_return('token') + + allow(Gitlab.config.registry).to receive_messages(registry_settings) + load 'lib/gitlab/auth.rb' end def stub_container_registry_tags(repository: :any, tags:) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 71b9deeabc3..6e5b9700b54 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -3,6 +3,8 @@ require 'rspec/mocks' module TestEnv extend self + ComponentFailedToInstallError = Class.new(StandardError) + # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => '2d1096e', @@ -63,6 +65,11 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) + unless Rails.env.test? + puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n" + exit 1 + end + # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -122,50 +129,23 @@ module TestEnv end def setup_gitlab_shell - puts "\n==> Setting up Gitlab Shell..." - start = Time.now - gitlab_shell_dir = Gitlab.config.gitlab_shell.path - shell_needs_update = component_needs_update?(gitlab_shell_dir, - Gitlab::Shell.version_required) - - unless !shell_needs_update || system('rake', 'gitlab:shell:install') - puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n" - FileUtils.rm_rf(gitlab_shell_dir) - exit 1 - end - - puts " GitLab Shell setup in #{Time.now - start} seconds...\n" + component_timed_setup('GitLab Shell', + install_dir: Gitlab.config.gitlab_shell.path, + version: Gitlab::Shell.version_required, + task: 'gitlab:shell:install') end def setup_gitaly - puts "\n==> Setting up Gitaly..." - start = Time.now socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) - if gitaly_dir_stale?(gitaly_dir) - puts " Gitaly is outdated, cleaning up #{gitaly_dir}!" - FileUtils.rm_rf(gitaly_dir) - end - - gitaly_needs_update = component_needs_update?(gitaly_dir, - Gitlab::GitalyClient.expected_server_version) + component_timed_setup('Gitaly', + install_dir: gitaly_dir, + version: Gitlab::GitalyClient.expected_server_version, + task: "gitlab:gitaly:install[#{gitaly_dir}]") do - unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") - puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n" - FileUtils.rm_rf(gitaly_dir) - exit 1 + start_gitaly(gitaly_dir) end - - start_gitaly(gitaly_dir) - puts " Gitaly setup in #{Time.now - start} seconds...\n" - end - - def gitaly_dir_stale?(dir) - gitaly_executable = File.join(dir, 'gitaly') - return false unless File.exist?(gitaly_executable) - - File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION')) end def start_gitaly(gitaly_dir) @@ -320,6 +300,40 @@ module TestEnv end end + def component_timed_setup(component, install_dir:, version:, task:) + puts "\n==> Setting up #{component}..." + start = Time.now + + ensure_component_dir_name_is_correct!(component, install_dir) + + if component_needs_update?(install_dir, version) + # Cleanup the component entirely to ensure we start fresh + FileUtils.rm_rf(install_dir) + unless system('rake', task) + raise ComponentFailedToInstallError + end + end + + yield if block_given? + + rescue ComponentFailedToInstallError + puts "\n#{component} failed to install, cleaning up #{install_dir}!\n" + FileUtils.rm_rf(install_dir) + exit 1 + ensure + puts " #{component} setup in #{Time.now - start} seconds...\n" + end + + def ensure_component_dir_name_is_correct!(component, path) + actual_component_dir_name = File.basename(path) + expected_component_dir_name = component.parameterize + + unless actual_component_dir_name == expected_component_dir_name + puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n" + exit 1 + end + end + def component_needs_update?(component_folder, expected_version) version = File.read(File.join(component_folder, 'VERSION')).strip diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb index f2c19c7642a..7724d54c569 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/ci/lints/show.html.haml_spec.rb @@ -4,7 +4,7 @@ describe 'ci/lints/show' do include Devise::Test::ControllerHelpers describe 'XSS protection' do - let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } before do assign(:status, true) assign(:builds, config_processor.builds) @@ -59,7 +59,7 @@ describe 'ci/lints/show' do } end - let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } context 'when the content is valid' do before do diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index b17bc6692f3..c5f455b8948 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -1,16 +1,28 @@ require 'spec_helper' describe 'layouts/nav/sidebar/_project' do + let(:project) { create(:project, :repository) } + + before do + assign(:project, project) + assign(:repository, project.repository) + allow(view).to receive(:current_ref).and_return('master') + + allow(view).to receive(:can?).and_return(true) + end + + describe 'issue boards' do + it 'has boards tab when multiple issue boards available' do + render + + expect(rendered).to have_css('a[title="Board"]') + end + end + describe 'container registry tab' do before do - project = create(:project, :repository) stub_container_registry_config(enabled: true) - assign(:project, project) - assign(:repository, project.repository) - allow(view).to receive(:current_ref).and_return('master') - - allow(view).to receive(:can?).and_return(true) allow(controller).to receive(:controller_name) .and_return('repositories') allow(controller).to receive(:controller_path) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 6f9ddb6c63c..f7b67b8efc6 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -31,8 +31,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id, :gc, lease_key, lease_uuid) end @@ -77,8 +77,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end |