diff options
Diffstat (limited to 'spec')
66 files changed, 1443 insertions, 163 deletions
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 56ecf2bb644..cfe18dd4b6c 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' describe HealthCheckController do + include StubENV + let(:token) { current_application_settings.health_check_access_token } let(:json_response) { JSON.parse(response.body) } let(:xml_response) { Hash.from_xml(response.body)['hash'] } + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + describe 'GET #index' do context 'when services are up but NO access token' do it 'returns a not found page' do diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index ed4acca23f1..c3b4aff55ba 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -16,6 +16,10 @@ FactoryGirl.define do is_shared true end + trait :specific do + is_shared false + end + trait :inactive do active false end diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index ece6beb9fa9..86f51ffca99 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,8 +1,9 @@ FactoryGirl.define do - factory :group do + factory :group, class: Group, parent: :namespace do sequence(:name) { |n| "group#{n}" } path { name.downcase.gsub(/\s/, '_') } type 'Group' + owner nil trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index a10ba629760..476f2a37793 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -13,6 +13,7 @@ FactoryGirl.define do factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_project_snippet, traits: [:on_project_snippet] + factory :note_on_personal_snippet, traits: [:on_personal_snippet] factory :system_note, traits: [:system] factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote @@ -70,6 +71,11 @@ FactoryGirl.define do noteable { create(:project_snippet, project: project) } end + trait :on_personal_snippet do + noteable { create(:personal_snippet) } + project nil + end + trait :system do system true end diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index 66044b44495..e8e080ce3e2 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -1,10 +1,13 @@ require 'rails_helper' feature 'Admin disables Git access protocol', feature: true do + include StubENV + let(:project) { create(:empty_project, :empty_repo) } let(:admin) { create(:admin) } background do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') login_as(admin) end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index dec2dedf2b5..f7e49a56deb 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -1,9 +1,11 @@ require 'spec_helper' feature "Admin Health Check", feature: true do + include StubENV include WaitForAjax before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') login_as :admin end @@ -12,11 +14,12 @@ feature "Admin Health Check", feature: true do visit admin_health_check_path end - it { page.has_text? 'Health Check' } - it { page.has_text? 'Health information can be retrieved' } - it 'has a health check access token' do + page.has_text? 'Health Check' + page.has_text? 'Health information can be retrieved' + token = current_application_settings.health_check_access_token + expect(page).to have_content("Access token is #{token}") expect(page).to have_selector('#health-check-token', text: token) end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index d92c66b689d..f05fbe3d062 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper' describe "Admin Runners" do + include StubENV + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') login_as :admin end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 47fa2f14307..de42ab81fac 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper' feature 'Admin updates settings', feature: true do - before(:each) do + include StubENV + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') login_as :admin visit admin_application_settings_path end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 661fb761809..855247de2ea 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -1,7 +1,12 @@ require 'rails_helper' feature 'Admin uses repository checks', feature: true do - before { login_as :admin } + include StubENV + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + login_as :admin + end scenario 'to trigger a single check' do project = create(:empty_project) @@ -29,7 +34,7 @@ feature 'Admin uses repository checks', feature: true do scenario 'to clear all repository checks', js: true do visit admin_application_settings_path - + expect(RepositoryCheck::ClearWorker).to receive(:perform_async) click_link 'Clear all repository checks' diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 16dcc487812..8a155c3bfc5 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -134,14 +134,14 @@ describe 'Dropdown assignee', js: true, feature: true do click_button 'Assigned to me' end - expect(filtered_search.value).to eq("assignee:#{user.to_reference}") + expect(filtered_search.value).to eq("assignee:#{user.to_reference} ") end it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ") end it 'fills in the assignee username when the assignee has been filtered' do @@ -149,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do click_assignee(user.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user.username}") + expect(filtered_search.value).to eq("assignee:@#{user.username} ") end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:none") + expect(filtered_search.value).to eq("assignee:none ") end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 464749d01e3..a5d5d9d4c5e 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -121,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do click_author(user_jacob.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + expect(filtered_search.value).to eq("author:@#{user_jacob.username} ") end it 'fills in the author username when the author has been filtered' do click_author(user.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user.username}") + expect(filtered_search.value).to eq("author:@#{user.username} ") end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 89c144141c9..f09ad2dd86b 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do visit namespace_project_issues_path(project.namespace, project) end + describe 'keyboard navigation' do + it 'selects label' do + send_keys_to_filtered_search('label:') + + filtered_search.native.send_keys(:down, :down, :enter) + + expect(filtered_search.value).to eq("label:~#{special_label.name} ") + end + end + describe 'behavior' do it 'opens when the search bar has label:' do filtered_search.set('label:') @@ -159,7 +169,7 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{bug_label.title}") + expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name when the label is partially filled' do @@ -167,49 +177,49 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{bug_label.title}") + expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") end it 'fills in the label name with special characters' do click_label(special_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{special_label.title}") + expect(filtered_search.value).to eq("label:~#{special_label.title} ") end it 'selects `no label`' do find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:none") + expect(filtered_search.value).to eq("label:none ") end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index e5a271b663f..134e58ad586 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") end it 'fills in the milestone name when the milestone is partially filled' do @@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ") end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ") end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ") end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ") end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ") end it 'selects `no milestone`' do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:none") + expect(filtered_search.value).to eq("milestone:none ") end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:upcoming") + expect(filtered_search.value).to eq("milestone:upcoming ") end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 1cdac520181..f48a0193545 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -539,7 +539,7 @@ describe 'Filter issues', js: true, feature: true do click_button user2.username end - expect(filtered_search.value).to eq("author:@#{user2.username}") + expect(filtered_search.value).to eq("author:@#{user2.username} ") end it 'changes label' do @@ -551,7 +551,7 @@ describe 'Filter issues', js: true, feature: true do click_button label.name end - expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}") + expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ") end it 'changes label correctly space is in previous label' do @@ -563,7 +563,7 @@ describe 'Filter issues', js: true, feature: true do click_button label.name end - expect(filtered_search.value).to eq("label:~#{label.name}") + expect(filtered_search.value).to eq("label:~#{label.name} ") end end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 56b1d354eb0..90eb60eb337 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do left_style.to_s.gsub('left: ', '').to_f end + describe 'keyboard navigation' do + it 'makes item active' do + filtered_search.native.send_keys(:down) + + page.within '#js-dropdown-hint' do + expect(page).to have_selector('.dropdown-active') + end + end + + it 'selects item' do + filtered_search.native.send_keys(:down, :down, :enter) + + expect(filtered_search.value).to eq('author:') + end + end + describe 'clear search button' do it 'clears text' do search_text = 'search_text' diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 8771cc8e157..741ca95f1ca 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -68,6 +68,22 @@ describe 'New/edit issue', feature: true, js: true do end end end + + it 'correctly updates the dropdown toggle when removing a label' do + click_button 'Labels' + + page.within '.dropdown-menu-labels' do + click_link label.title + end + + expect(find('.js-label-select')).to have_content(label.title) + + page.within '.dropdown-menu-labels' do + click_link label.title + end + + expect(find('.js-label-select')).to have_content('Labels') + end end context 'edit issue' do diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb new file mode 100644 index 00000000000..d0bafc6168c --- /dev/null +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do + let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + + let(:project) { create(:empty_project) } + + background do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + context 'admin user' do + before do + login_as(:admin) + end + + context 'moving the namespace' do + scenario 'removes the export file' do + setup_export_project + + old_export_path = project.export_path.dup + + expect(File).to exist(old_export_path) + + project.namespace.update(path: 'new_path') + + expect(File).not_to exist(old_export_path) + end + end + + context 'deleting the namespace' do + scenario 'removes the export file' do + setup_export_project + + old_export_path = project.export_path.dup + + expect(File).to exist(old_export_path) + + project.namespace.destroy + + expect(File).not_to exist(old_export_path) + end + end + + def setup_export_project + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Export project') + + click_link 'Export project' + + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Download export') + end + end +end diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb new file mode 100644 index 00000000000..b6728960fb8 --- /dev/null +++ b/spec/features/projects/merge_request_button_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +feature 'Merge Request button', feature: true do + shared_examples 'Merge Request button only shown when allowed' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:forked_project) { create(:project, :public, forked_from_project: project) } + + context 'not logged in' do + it 'does not show Create Merge Request button' do + visit url + + within("#content-body") do + expect(page).not_to have_link(label) + end + end + end + + context 'logged in as developer' do + before do + login_as(user) + project.team << [user, :developer] + end + + it 'shows Create Merge Request button' do + href = new_namespace_project_merge_request_path(project.namespace, + project, + merge_request: { source_branch: 'feature', + target_branch: 'master' }) + + visit url + + within("#content-body") do + expect(page).to have_link(label, href: href) + end + end + + context 'merge requests are disabled' do + before do + project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + end + + it 'does not show Create Merge Request button' do + visit url + + within("#content-body") do + expect(page).not_to have_link(label) + end + end + end + end + + context 'logged in as non-member' do + before do + login_as(user) + end + + it 'does not show Create Merge Request button' do + visit url + + within("#content-body") do + expect(page).not_to have_link(label) + end + end + + context 'on own fork of project' do + let(:user) { forked_project.owner } + + it 'shows Create Merge Request button' do + href = new_namespace_project_merge_request_path(forked_project.namespace, + forked_project, + merge_request: { source_branch: 'feature', + target_branch: 'master' }) + + visit fork_url + + within("#content-body") do + expect(page).to have_link(label, href: href) + end + end + end + end + end + + context 'on branches page' do + it_behaves_like 'Merge Request button only shown when allowed' do + let(:label) { 'Merge Request' } + let(:url) { namespace_project_branches_path(project.namespace, project) } + let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) } + end + end + + context 'on compare page' do + it_behaves_like 'Merge Request button only shown when allowed' do + let(:label) { 'Create Merge Request' } + let(:url) { namespace_project_compare_path(project.namespace, project, from: 'master', to: 'feature') } + let(:fork_url) { namespace_project_compare_path(forked_project.namespace, forked_project, from: 'master', to: 'feature') } + end + end + + context 'on commits page' do + it_behaves_like 'Merge Request button only shown when allowed' do + let(:label) { 'Create Merge Request' } + let(:url) { namespace_project_commits_path(project.namespace, project, 'feature') } + let(:fork_url) { namespace_project_commits_path(forked_project.namespace, forked_project, 'feature') } + end + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index a05b83959fb..0fe5a897565 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -211,4 +211,44 @@ describe "Search", feature: true do end end end + + describe 'search for commits' do + 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(namespace_project_commit_path(project.namespace, 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(namespace_project_commit_path(project.namespace, 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 end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index abb27c90e0a..a5d14aa19f1 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -36,6 +36,19 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:nested_tasks_markdown) do + <<-EOT.strip_heredoc + - [ ] Task a + - [x] Task a.1 + - [ ] Task a.2 + - [ ] Task b + + 1. [ ] Task 1 + 1. [ ] Task 1.1 + 1. [x] Task 1.2 + EOT + end + before do Warden.test_mode! @@ -123,6 +136,35 @@ feature 'Task Lists', feature: true do expect(page).to have_content("1 of 1 task completed") end end + + describe 'nested tasks', js: true do + let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } + + before { visit_issue(project, issue) } + + it 'renders' do + expect(page).to have_selector('ul.task-list', count: 2) + expect(page).to have_selector('li.task-list-item', count: 7) + expect(page).to have_selector('ul input[checked]', count: 1) + expect(page).to have_selector('ol input[checked]', count: 1) + end + + it 'solves tasks' do + expect(page).to have_content("2 of 7 tasks completed") + + page.find('li.task-list-item', text: 'Task b').find('input').click + page.find('li.task-list-item ul li.task-list-item', text: 'Task a.2').find('input').click + page.find('li.task-list-item ol li.task-list-item', text: 'Task 1.1').find('input').click + + expect(page).to have_content("5 of 7 tasks completed") + + visit_issue(project, issue) # reload to see new system notes + + expect(page).to have_content('marked the task Task b as complete') + expect(page).to have_content('marked the task Task a.2 as complete') + expect(page).to have_content('marked the task Task 1.1 as complete') + end + end end describe 'for Notes' do @@ -236,7 +278,7 @@ feature 'Task Lists', feature: true do expect(page).to have_content("2 of 6 tasks completed") end end - + describe 'single incomplete task' do let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb new file mode 100644 index 00000000000..bb595162370 --- /dev/null +++ b/spec/initializers/metrics_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require_relative '../../config/initializers/metrics' + +describe 'instrument_classes', lib: true do + let(:config) { double(:config) } + + before do + allow(config).to receive(:instrument_method) + allow(config).to receive(:instrument_methods) + allow(config).to receive(:instrument_instance_methods) + end + + it 'can autoload and instrument all files' do + expect { instrument_classes(config) }.not_to raise_error + end +end diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index cf19aa05031..a2d57824585 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -21,7 +21,6 @@ messages = $('.abuse-reports .message'); }); - it('should truncate long messages', () => { const $longMessage = findMessage('LONG MESSAGE'); expect($longMessage.data('original-message')).toEqual(jasmine.anything()); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 21241116e29..95796f23894 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -33,7 +33,6 @@ describe('Rollback Component', () => { expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); }); - it('Should render Rollback label when isLastDeployment is false', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index d0d27ceb4a6..4bd45eb457d 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -31,7 +31,7 @@ it('should add tokenName and tokenValue', () => { gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - expect(getInputValue()).toBe('label:none'); + expect(getInputValue()).toBe('label:none '); }); }); @@ -45,13 +45,13 @@ it('should replace tokenValue', () => { setInputValue('author:roo'); gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); - expect(getInputValue()).toBe('author:@root'); + expect(getInputValue()).toBe('author:@root '); }); it('should add tokenValues containing spaces', () => { setInputValue('label:~"test'); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - expect(getInputValue()).toBe('label:~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\' '); }); }); }); diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index 6b48d82cb23..99cebb32a8b 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -62,4 +62,30 @@ describe('GfmAutoComplete', function () { }); }); }); + + describe('isLoading', function () { + it('should be true with loading data object item', function () { + expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true); + }); + + it('should be true with loading data array', function () { + expect(GfmAutoComplete.isLoading(['loading'])).toBe(true); + }); + + it('should be true with loading data object array', function () { + expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true); + }); + + it('should be false with actual array data', function () { + expect(GfmAutoComplete.isLoading([ + { title: 'Foo' }, + { title: 'Bar' }, + { title: 'Qux' }, + ])).toBe(false); + }); + + it('should be false with actual data item', function () { + expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false); + }); + }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 031f9ca03c9..1ce8f28e568 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -52,5 +52,22 @@ expect(value).toBe(null); }); }); + + describe('gl.utils.normalizedHeaders', () => { + it('should upperCase all the header keys to keep them consistent', () => { + const apiHeaders = { + 'X-Something-Workhorse': { workhorse: 'ok' }, + 'x-something-nginx': { nginx: 'ok' }, + }; + + const normalized = gl.utils.normalizeHeaders(apiHeaders); + + const WORKHORSE = 'X-SOMETHING-WORKHORSE'; + const NGINX = 'X-SOMETHING-NGINX'; + + expect(normalized[WORKHORSE].workhorse).toBe('ok'); + expect(normalized[NGINX].nginx).toBe('ok'); + }); + }); }); })(); diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 5bfeb82e738..3e1ac9fb2b2 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end + context 'when a project is not specified' do + let(:project) { nil } + + it 'does not link a User' do + doc = reference_filter("Hey #{reference}") + + expect(doc).not_to include('a') + end + + context 'when skip_project_check set to true' do + it 'links to a User' do + doc = reference_filter("Hey #{reference}", skip_project_check: true) + + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'does not link users using @all reference' do + doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true) + + expect(doc).not_to include('a') + end + end + end + describe '#namespaces' do it 'returns a Hash containing all Namespaces' do document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 004341ffd02..b01c4805a34 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -1,36 +1,64 @@ require 'spec_helper' describe Gitlab::CurrentSettings do + include StubENV + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + describe '#current_application_settings' do - it 'attempts to use cached values first' do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) - expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) - expect(ApplicationSetting).not_to receive(:last) + context 'with DB available' do + before do + allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) + end - expect(current_application_settings).to be_a(ApplicationSetting) - end + it 'attempts to use cached values first' do + expect(ApplicationSetting).to receive(:current) + expect(ApplicationSetting).not_to receive(:last) + + expect(current_application_settings).to be_a(ApplicationSetting) + end - it 'does not attempt to connect to DB or Redis' do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false) - expect(ApplicationSetting).not_to receive(:current) - expect(ApplicationSetting).not_to receive(:last) + it 'falls back to DB if Redis returns an empty value' do + expect(ApplicationSetting).to receive(:last).and_call_original - expect(current_application_settings).to eq fake_application_settings + expect(current_application_settings).to be_a(ApplicationSetting) + end + + it 'falls back to DB if Redis fails' do + expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError) + expect(ApplicationSetting).to receive(:last).and_call_original + + expect(current_application_settings).to be_a(ApplicationSetting) + end end - it 'falls back to DB if Redis returns an empty value' do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) - expect(ApplicationSetting).to receive(:last).and_call_original + context 'with DB unavailable' do + before do + allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false) + end - expect(current_application_settings).to be_a(ApplicationSetting) + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) + expect(ApplicationSetting).not_to receive(:last) + + expect(current_application_settings).to be_a(OpenStruct) + end end - it 'falls back to DB if Redis fails' do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) - expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError) - expect(ApplicationSetting).to receive(:last).and_call_original + context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true') + end + + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) + expect(ApplicationSetting).not_to receive(:last) - expect(current_application_settings).to be_a(ApplicationSetting) + expect(current_application_settings).to be_a(ApplicationSetting) + expect(current_application_settings).not_to be_persisted + end end end end diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb index 19298e261e3..9d806fc524d 100644 --- a/spec/lib/gitlab/email/email_shared_blocks.rb +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -18,7 +18,7 @@ shared_context :email_shared_context do end end -shared_examples :email_shared_examples do +shared_examples :reply_processing_shared_examples do context "when the user could not be found" do before do user.destroy diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index cb3651e3845..08897a4c310 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 48660d1dd1b..cebbeff50cf 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb new file mode 100644 index 00000000000..a444257754b --- /dev/null +++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do + include_context :email_shared_context + + before do + stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo') + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") } + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:noteable) { create(:issue, project: project) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context 'when notification concerns a commit' do + let(:commit) { create(:commit, project: project) } + let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) } + + it 'handler does not raise an error' do + expect { receiver.execute }.not_to raise_error + end + end + + context 'user is unsubscribed' do + it 'leaves user unsubscribed' do + expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false) + end + end + + context 'user is subscribed' do + before do + noteable.subscribe(user) + end + + it 'unsubscribes user from notable' do + expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false) + end + end + + context 'when the noteable could not be found' do + before do + noteable.destroy + end + + it 'raises a NoteableNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context 'when no sent notification for the mail key could be found' do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it 'raises a SentNotificationNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end +end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 1dcf2c0668b..7e951e3fcdd 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do end end + describe 'self.supports_wildcard?' do + context 'address contains the wildard placeholder' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'confirms that wildcard is supported' do + expect(described_class.supports_wildcard?).to be_truthy + end + end + + context "address doesn't contain the wildcard placeholder" do + before do + stub_incoming_email_setting(address: 'replies@example.com') + end + + it 'returns that wildcard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + + context 'address is not set' do + before do + stub_incoming_email_setting(address: nil) + end + + it 'returns that wildard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + end + + context 'self.unsubscribe_address' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'returns the address with interpolated reply key and unsubscribe suffix' do + expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com') + end + end + context "self.reply_address" do before do stub_incoming_email_setting(address: "replies+%{key}@example.com") diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb new file mode 100644 index 00000000000..780f5b1f8d7 --- /dev/null +++ b/spec/lib/gitlab/job_waiter_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::JobWaiter do + describe '#wait' do + let(:waiter) { described_class.new(%w(a)) } + it 'returns when all jobs have been completed' do + expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a)). + and_return(true) + + expect(waiter).not_to receive(:sleep) + + waiter.wait + end + + it 'sleeps between checking the job statuses' do + expect(Gitlab::SidekiqStatus).to receive(:all_completed?). + with(%w(a)). + and_return(false, true) + + expect(waiter).to receive(:sleep).with(described_class::INTERVAL) + + waiter.wait + end + + it 'returns when timing out' do + expect(waiter).not_to receive(:sleep) + waiter.wait(0) + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 14ee386dba6..d94eb52f838 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -178,4 +178,119 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.objects('notes')).not_to include note end end + + # Examples for commit access level test + # + # params: + # * search_phrase + # * commit + # + shared_examples 'access restricted commits' do + context 'when project is internal' do + let(:project) { create(:project, :internal) } + + it 'does not search if user is not authenticated' do + commits = described_class.new(nil, project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + it 'searches if user is authenticated' do + commits = described_class.new(user, project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + context 'when project is private' do + let!(:creator) { create(:user, username: 'private-project-author') } + let!(:private_project) { create(:project, :private, creator: creator, namespace: creator.namespace) } + let(:team_master) do + user = create(:user, username: 'private-project-master') + private_project.team << [user, :master] + user + end + let(:team_reporter) do + user = create(:user, username: 'private-project-reporter') + private_project.team << [user, :reporter] + user + end + + it 'does not show commit to stranger' do + commits = described_class.new(nil, private_project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + context 'team access' do + it 'shows commit to creator' do + commits = described_class.new(creator, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to master' do + commits = described_class.new(team_master, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to reporter' do + commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + end + end + + describe 'commit search' do + context 'by commit message' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + let(:message) { 'Sorry, I did a mistake' } + + it 'finds commit by message' do + commits = described_class.new(user, project, message).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'handles when no commit match' do + commits = described_class.new(user, project, 'not really an existing description').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { message } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + end + end + + context 'by commit hash' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('0b4bc9a') } + commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + commit_hashes.each do |type, commit_hash| + it "shows commit by #{type} hash id" do + commits = described_class.new(user, project, commit_hash).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + it 'handles not existing commit hash correctly' do + commits = described_class.new(user, project, 'deadbeef').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { '0b4bc9a49' } + let(:commit) { project.repository.commit('0b4bc9a') } + end + end + end end diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb new file mode 100644 index 00000000000..287bf62d9bd --- /dev/null +++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::SidekiqStatus::ClientMiddleware do + describe '#call' do + it 'tracks the job in Redis' do + expect(Gitlab::SidekiqStatus).to receive(:set).with('123') + + described_class.new. + call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } + end + end +end diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb new file mode 100644 index 00000000000..80728197b8c --- /dev/null +++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Gitlab::SidekiqStatus::ServerMiddleware do + describe '#call' do + it 'stops tracking of a job upon completion' do + expect(Gitlab::SidekiqStatus).to receive(:unset).with('123') + + ret = described_class.new. + call(double(:worker), { 'jid' => '123' }, double(:queue)) { 10 } + + expect(ret).to eq(10) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb new file mode 100644 index 00000000000..0aa36a3416b --- /dev/null +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::SidekiqStatus do + describe '.set', :redis do + it 'stores the job ID' do + described_class.set('123') + + key = described_class.key_for('123') + + Sidekiq.redis do |redis| + expect(redis.exists(key)).to eq(true) + expect(redis.ttl(key) > 0).to eq(true) + end + end + end + + describe '.unset', :redis do + it 'removes the job ID' do + described_class.set('123') + described_class.unset('123') + + key = described_class.key_for('123') + + Sidekiq.redis do |redis| + expect(redis.exists(key)).to eq(false) + end + end + end + + describe '.all_completed?', :redis do + it 'returns true if all jobs have been completed' do + expect(described_class.all_completed?(%w(123))).to eq(true) + end + + it 'returns false if a job has not yet been completed' do + described_class.set('123') + + expect(described_class.all_completed?(%w(123 456))).to eq(false) + end + end + + describe '.key_for' do + it 'returns the key for a job ID' do + key = described_class.key_for('123') + + expect(key).to be_an_instance_of(String) + expect(key).to include('123') + end + end +end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index d3c3b800b94..369e55f61f1 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do end describe 'push to protected branch' do - let(:branch) { create :protected_branch, project: project } + let(:branch) { create :protected_branch, project: project, name: "test" } + let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } it 'returns true if user is a master' do project.team << [user, :master] @@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do expect(access.can_push_to_branch?(branch.name)).to be_falsey end + + it 'returns true if branch does not exist and user has permission to merge' do + project.team << [user, :developer] + + expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy + end end describe 'push to protected branch if allowed for developers' do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 1bdf005c823..4d57efd3c53 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,6 +171,33 @@ describe Ability, lib: true do end end + describe '.users_that_can_read_personal_snippet' do + def users_for_snippet(snippet) + described_class.users_that_can_read_personal_snippet(users, snippet) + end + + let(:users) { create_list(:user, 3) } + let(:author) { users[0] } + + it 'private snippet is readable only by its author' do + snippet = create(:personal_snippet, :private, author: author) + + expect(users_for_snippet(snippet)).to match_array([author]) + end + + it 'internal snippet is readable by all registered users' do + snippet = create(:personal_snippet, :public, author: author) + + expect(users_for_snippet(snippet)).to match_array(users) + end + + it 'public snippet is readable by all users' do + snippet = create(:personal_snippet, :public, author: author) + + expect(users_for_snippet(snippet)).to match_array(users) + end + end + describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0d425ab7fd4..b2202f0fd44 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -351,4 +351,22 @@ eos expect(commit).not_to be_work_in_progress end end + + describe '.valid_hash?' do + it 'checks hash contents' do + expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true + expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false + expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false + expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false + expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false + expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false + end + + it 'checks hash length' do + expect(described_class.valid_hash?('a' * 6)).to be false + expect(described_class.valid_hash?('a' * 7)).to be true + expect(described_class.valid_hash?('a' * 40)).to be true + expect(described_class.valid_hash?('a' * 41)).to be false + end + end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 132858950d5..b73028f0bc0 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -30,12 +30,20 @@ describe Issue, "Mentionable" do describe '#mentioned_users' do let!(:user) { create(:user, username: 'stranger') } let!(:user2) { create(:user, username: 'john') } - let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") } + let!(:user3) { create(:user, username: 'jim') } + let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") } subject { issue.mentioned_users } - it { is_expected.to include(user) } - it { is_expected.not_to include(user2) } + it { expect(subject).to contain_exactly(user) } + + context 'when a note on personal snippet' do + let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") } + + subject { note.mentioned_users } + + it { expect(subject).to contain_exactly(user, user3) } + end end describe '#referenced_mentionables' do @@ -138,6 +146,16 @@ describe Issue, "Mentionable" do issue.update_attributes(description: issues[1].to_reference) issue.create_new_cross_references! end + + it 'notifies new references from project snippet note' do + snippet = create(:snippet, project: project) + note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author) + + expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args) + + note.update_attributes(note: issues[1].to_reference) + note.create_new_cross_references! + end end def create_issue(description:) diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index b556135532f..30443534cca 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -68,4 +68,14 @@ describe Group, 'Routable' do end end end + + describe '.member_descendants' do + let!(:user) { create(:user) } + let!(:nested_group) { create(:group, parent: group) } + + before { group.add_owner(user) } + subject { described_class.member_descendants(user.id) } + + it { is_expected.to eq([nested_group]) } + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 45fe927202b..9ca50555191 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -81,13 +81,19 @@ describe Group, models: true do describe 'public_only' do subject { described_class.public_only.to_a } - it{ is_expected.to eq([group]) } + it { is_expected.to eq([group]) } end describe 'public_and_internal_only' do subject { described_class.public_and_internal_only.to_a } - it{ is_expected.to match_array([group, internal_group]) } + it { is_expected.to match_array([group, internal_group]) } + end + + describe 'non_public_only' do + subject { described_class.non_public_only.to_a } + + it { is_expected.to match_array([private_group, internal_group]) } end end @@ -269,6 +275,12 @@ describe Group, models: true do it 'returns the canonical URL' do expect(group.web_url).to include("groups/#{group.name}") end + + context 'nested group' do + let(:nested_group) { create(:group, :nested) } + + it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") } + end end describe 'nested group' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 600538ff5f4..8d613a88ca0 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -5,6 +5,8 @@ describe Namespace, models: true do it { is_expected.to have_many :projects } it { is_expected.to have_many :project_statistics } + it { is_expected.to belong_to :parent } + it { is_expected.to have_many :children } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } @@ -117,6 +119,7 @@ describe Namespace, models: true do new_path = @namespace.path + "_new" allow(@namespace).to receive(:path_was).and_return(@namespace.path) allow(@namespace).to receive(:path).and_return(new_path) + expect(@namespace).to receive(:remove_exports!) expect(@namespace.move_dir).to be_truthy end @@ -139,11 +142,17 @@ describe Namespace, models: true do let!(:project) { create(:project, namespace: namespace) } let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } - before { namespace.destroy } - it "removes its dirs when deleted" do + namespace.destroy + expect(File.exist?(path)).to be(false) end + + it 'removes the exports folder' do + expect(namespace).to receive(:remove_exports!) + + namespace.destroy + end end describe '.find_by_path_or_name' do @@ -182,17 +191,31 @@ describe Namespace, models: true do it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } end - describe '#parents' do + describe '#ancestors' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let(:deep_nested_group) { create(:group, parent: nested_group) } let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - it 'returns the correct parents' do - expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) - expect(deep_nested_group.parents).to eq([group, nested_group]) - expect(nested_group.parents).to eq([group]) - expect(group.parents).to eq([]) + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.ancestors).to eq([group, nested_group]) + expect(nested_group.ancestors).to eq([group]) + expect(group.ancestors).to eq([]) + end + end + + describe '#descendants' do + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.descendants.to_a).to eq([]) + expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group]) + expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group]) + expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group]) end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 310fecd8a5c..1b8ae356f1f 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -52,6 +52,19 @@ describe Note, models: true do subject { create(:note) } it { is_expected.to be_valid } end + + context 'when project is missing for a project related note' do + subject { build(:note, project: nil, noteable: build_stubbed(:issue)) } + it { is_expected.to be_invalid } + end + + context 'when noteable is a personal snippet' do + subject { build(:note_on_personal_snippet) } + + it 'is valid without project' do + is_expected.to be_valid + end + end end describe "Commit notes" do @@ -139,6 +152,7 @@ describe Note, models: true do with([{ text: note1.note, context: { + skip_project_check: false, pipeline: :note, cache_key: [note1, "note"], project: note1.project, @@ -150,6 +164,7 @@ describe Note, models: true do with([{ text: note2.note, context: { + skip_project_check: false, pipeline: :note, cache_key: [note2, "note"], project: note2.project, @@ -306,4 +321,70 @@ describe Note, models: true do end end end + + describe '#for_personal_snippet?' do + it 'returns false for a project snippet note' do + expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy + end + + it 'returns true for a personal snippet note' do + expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy + end + end + + describe '#to_ability_name' do + it 'returns snippet for a project snippet note' do + expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet') + end + + it 'returns personal_snippet for a personal snippet note' do + expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet') + end + + it 'returns merge_request for an MR note' do + expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request') + end + + it 'returns issue for an issue note' do + expect(build(:note_on_issue).to_ability_name).to eq('issue') + end + + it 'returns issue for a commit note' do + expect(build(:note_on_commit).to_ability_name).to eq('commit') + end + end + + describe '#cache_markdown_field' do + let(:html) { '<p>some html</p>'} + + context 'note for a project snippet' do + let(:note) { build(:note_on_project_snippet) } + + before do + expect(Banzai::Renderer).to receive(:cacheless_render_field). + with(note, :note, { skip_project_check: false }).and_return(html) + + note.save + end + + it 'creates a note' do + expect(note.note_html).to eq(html) + end + end + + context 'note for a personal snippet' do + let(:note) { build(:note_on_personal_snippet) } + + before do + expect(Banzai::Renderer).to receive(:cacheless_render_field). + with(note, :note, { skip_project_check: true }).and_return(html) + + note.save + end + + it 'creates a note' do + expect(note.note_html).to eq(html) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8048e86fc3a..646a1311462 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -832,6 +832,26 @@ describe Project, models: true do it { expect(project.builds_enabled?).to be_truthy } end + describe '.with_shared_runners' do + subject { Project.with_shared_runners } + + context 'when shared runners are enabled for project' do + let!(:project) { create(:empty_project, shared_runners_enabled: true) } + + it "returns a project" do + is_expected.to eq([project]) + end + end + + context 'when shared runners are disabled for project' do + let!(:project) { create(:empty_project, shared_runners_enabled: false) } + + it "returns an empty array" do + is_expected.to be_empty + end + end + end + describe '.cached_count', caching: true do let(:group) { create(:group, :public) } let!(:project1) { create(:empty_project, :public, group: group) } @@ -974,6 +994,28 @@ describe Project, models: true do end end + describe '#shared_runners' do + let!(:runner) { create(:ci_runner, :shared) } + + subject { project.shared_runners } + + context 'when shared runners are enabled for project' do + let!(:project) { create(:empty_project, shared_runners_enabled: true) } + + it "returns a list of shared runners" do + is_expected.to eq([runner]) + end + end + + context 'when shared runners are disabled for project' do + let!(:project) { create(:empty_project, shared_runners_enabled: false) } + + it "returns a empty list" do + is_expected.to be_empty + end + end + end + describe '#visibility_level_allowed?' do let(:project) { create(:empty_project, :internal) } diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 8481a9bef16..dd2a5109abc 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -14,7 +14,7 @@ describe Route, models: true do it { is_expected.to validate_uniqueness_of(:path) } end - describe '#rename_children' do + describe '#rename_descendants' do let!(:nested_group) { create(:group, path: "test", parent: group) } let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } let!(:similar_group) { create(:group, path: 'gitlab-org') } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ca3d4ff0aa9..0adfc30fe58 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -797,14 +797,14 @@ describe User, models: true do describe '#avatar_type' do let(:user) { create(:user) } - it "is true if avatar is image" do + it 'is true if avatar is image' do user.update_attribute(:avatar, 'uploads/avatar.png') expect(user.avatar_type).to be_truthy end - it "is false if avatar is html page" do + it 'is false if avatar is html page' do user.update_attribute(:avatar, 'uploads/avatar.html') - expect(user.avatar_type).to eq(["only images allowed"]) + expect(user.avatar_type).to eq(['only images allowed']) end end @@ -926,8 +926,8 @@ describe User, models: true do end end - describe "#starred?" do - it "determines if user starred a project" do + describe '#starred?' do + it 'determines if user starred a project' do user = create :user project1 = create(:empty_project, :public) project2 = create(:empty_project, :public) @@ -953,8 +953,8 @@ describe User, models: true do end end - describe "#toggle_star" do - it "toggles stars" do + describe '#toggle_star' do + it 'toggles stars' do user = create :user project = create(:empty_project, :public) @@ -966,31 +966,44 @@ describe User, models: true do end end - describe "#sort" do + describe '#sort' do before do User.delete_all @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha' @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega' + @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta' end - it "sorts users by the recent sign-in time" do - expect(User.sort('recent_sign_in').first).to eq(@user) + context 'when sort by recent_sign_in' do + it 'sorts users by the recent sign-in time' do + expect(User.sort('recent_sign_in').first).to eq(@user) + end + + it 'pushes users who never signed in to the end' do + expect(User.sort('recent_sign_in').third).to eq(@user2) + end end - it "sorts users by the oldest sign-in time" do - expect(User.sort('oldest_sign_in').first).to eq(@user1) + context 'when sort by oldest_sign_in' do + it 'sorts users by the oldest sign-in time' do + expect(User.sort('oldest_sign_in').first).to eq(@user1) + end + + it 'pushes users who never signed in to the end' do + expect(User.sort('oldest_sign_in').third).to eq(@user2) + end end - it "sorts users in descending order by their creation time" do + it 'sorts users in descending order by their creation time' do expect(User.sort('created_desc').first).to eq(@user) end - it "sorts users in ascending order by their creation time" do - expect(User.sort('created_asc').first).to eq(@user1) + it 'sorts users in ascending order by their creation time' do + expect(User.sort('created_asc').first).to eq(@user2) end - it "sorts users by id in descending order when nil is passed" do - expect(User.sort(nil).first).to eq(@user1) + it 'sorts users by id in descending order when nil is passed' do + expect(User.sort(nil).first).to eq(@user2) end end @@ -1350,6 +1363,39 @@ describe User, models: true do end end + describe '#nested_groups' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong groups + create(:group).add_owner(create(:user)) + end + + it { expect(user.nested_groups).to eq([nested_group]) } + end + + describe '#nested_projects' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:project) { create(:project, namespace: group) } + let!(:nested_project) { create(:project, namespace: nested_group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong projects + other_project = create(:project, namespace: create(:group, :nested)) + other_project.add_developer(create(:user)) + end + + it { expect(user.nested_projects).to eq([nested_project]) } + end + describe '#refresh_authorized_projects', redis: true do let(:project1) { create(:empty_project) } let(:project2) { create(:empty_project) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 5c14db067a8..766234d7104 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -73,19 +73,14 @@ describe API::DeployKeys, api: true do post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } expect(response).to have_http_status(400) - expect(json_response['message']['key']).to eq([ - 'can\'t be blank', - 'is invalid' - ]) + expect(json_response['error']).to eq('key is missing') end it 'should not create a key without title' do post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' expect(response).to have_http_status(400) - expect(json_response['message']['title']).to eq([ - 'can\'t be blank' - ]) + expect(json_response['error']).to eq('title is missing') end it 'should create new ssh key' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index a3798c8cd6c..91202244227 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -337,8 +337,7 @@ describe API::Internal, api: true do context 'ssh access has been disabled' do before do - settings = ::ApplicationSetting.create_from_defaults - settings.update_attribute(:enabled_git_access_protocol, 'http') + stub_application_setting(enabled_git_access_protocol: 'http') end it 'rejects the SSH push' do @@ -360,8 +359,7 @@ describe API::Internal, api: true do context 'http access has been disabled' do before do - settings = ::ApplicationSetting.create_from_defaults - settings.update_attribute(:enabled_git_access_protocol, 'ssh') + stub_application_setting(enabled_git_access_protocol: 'ssh') end it 'rejects the HTTP push' do @@ -383,8 +381,7 @@ describe API::Internal, api: true do context 'web actions are always allowed' do it 'allows WEB push' do - settings = ::ApplicationSetting.create_from_defaults - settings.update_attribute(:enabled_git_access_protocol, 'ssh') + stub_application_setting(enabled_git_access_protocol: 'ssh') project.team << [user, :developer] push(key, project, 'web') diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 6f20ac49269..71a7994e544 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -627,6 +627,17 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(issue.title) expect(json_response.first['id']).to eq(issue.id) end + + it 'returns 403 if the user has no access to the merge request' do + project = create(:empty_project, :private) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.team << [guest, :guest] + + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + + expect(response).to have_http_status(403) + end end describe 'POST :id/merge_requests/:merge_request_id/subscription' do @@ -648,6 +659,15 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end end describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do @@ -669,6 +689,15 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end end describe 'Time tracking' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0f8d054b31e..0353ebea9e5 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -264,6 +264,18 @@ describe API::Notes, api: true do end end + context 'when user does not have access to read the noteable' do + it 'responds with 404' do + project = create(:empty_project, :private) { |p| p.add_guest(user) } + issue = create(:issue, :confidential, project: project) + + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'Foo' + + expect(response).to have_http_status(404) + end + end + context 'when user does not have access to create noteable' do let(:private_issue) { create(:issue, project: create(:empty_project, :private)) } diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 6fe695626ca..56dc017ce54 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -183,12 +183,25 @@ describe API::Todos, api: true do expect(response.status).to eq(404) end + + it 'returns an error if the issuable is not accessible' do + guest = create(:user) + project_1.team << [guest, :guest] + + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest) + + if issuable_type == 'merge_requests' + expect(response).to have_http_status(403) + else + expect(response).to have_http_status(404) + end + end end describe 'POST :id/issuable_type/:issueable_id/todo' do context 'for an issue' do it_behaves_like 'an issuable', 'issues' do - let(:issuable) { create(:issue, author: author_1, project: project_1) } + let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) } end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 270c23e3f19..8dbe5f0b025 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -91,6 +91,20 @@ describe Ci::API::Builds do expect { register_builds }.to change { runner.reload.contacted_at } end + context 'when concurrently updating build' do + before do + expect_any_instance_of(Ci::Build).to receive(:run!). + and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + end + + it 'returns a conflict' do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(409) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + end + end + context 'registry credentials' do let(:registry_credentials) do { 'type' => 'registry', diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 72978846e93..28b485e4b15 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -13,7 +13,12 @@ describe 'cycle analytics events' do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) - 3.times { create_cycle } + 3.times do |count| + Timecop.freeze(Time.now + count.days) do + create_cycle + end + end + deploy_master login_as(user) diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index a3fc23ba177..d9f774a1095 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' module Ci describe RegisterBuildService, services: true do - let!(:service) { RegisterBuildService.new } let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } @@ -19,29 +18,29 @@ module Ci pending_build.tag_list = ["linux"] pending_build.save specific_runner.tag_list = ["linux"] - expect(service.execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_build) end it "does not pick build with different tag" do pending_build.tag_list = ["linux"] pending_build.save specific_runner.tag_list = ["win32"] - expect(service.execute(specific_runner)).to be_falsey + expect(execute(specific_runner)).to be_falsey end it "picks build without tag" do - expect(service.execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_build) end it "does not pick build with tag" do pending_build.tag_list = ["linux"] pending_build.save - expect(service.execute(specific_runner)).to be_falsey + expect(execute(specific_runner)).to be_falsey end it "pick build without tag" do specific_runner.tag_list = ["win32"] - expect(service.execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_build) end end @@ -56,13 +55,13 @@ module Ci end it 'does not pick a build' do - expect(service.execute(shared_runner)).to be_nil + expect(execute(shared_runner)).to be_nil end end context 'for specific runner' do it 'does not pick a build' do - expect(service.execute(specific_runner)).to be_nil + expect(execute(specific_runner)).to be_nil end end end @@ -86,34 +85,34 @@ module Ci it 'prefers projects without builds first' do # it gets for one build from each of the projects - expect(service.execute(shared_runner)).to eq(build1_project1) - expect(service.execute(shared_runner)).to eq(build1_project2) - expect(service.execute(shared_runner)).to eq(build1_project3) + expect(execute(shared_runner)).to eq(build1_project1) + expect(execute(shared_runner)).to eq(build1_project2) + expect(execute(shared_runner)).to eq(build1_project3) # then it gets a second build from each of the projects - expect(service.execute(shared_runner)).to eq(build2_project1) - expect(service.execute(shared_runner)).to eq(build2_project2) + expect(execute(shared_runner)).to eq(build2_project1) + expect(execute(shared_runner)).to eq(build2_project2) # in the end the third build - expect(service.execute(shared_runner)).to eq(build3_project1) + expect(execute(shared_runner)).to eq(build3_project1) end it 'equalises number of running builds' do # after finishing the first build for project 1, get a second build from the same project - expect(service.execute(shared_runner)).to eq(build1_project1) + expect(execute(shared_runner)).to eq(build1_project1) build1_project1.reload.success - expect(service.execute(shared_runner)).to eq(build2_project1) + expect(execute(shared_runner)).to eq(build2_project1) - expect(service.execute(shared_runner)).to eq(build1_project2) + expect(execute(shared_runner)).to eq(build1_project2) build1_project2.reload.success - expect(service.execute(shared_runner)).to eq(build2_project2) - expect(service.execute(shared_runner)).to eq(build1_project3) - expect(service.execute(shared_runner)).to eq(build3_project1) + expect(execute(shared_runner)).to eq(build2_project2) + expect(execute(shared_runner)).to eq(build1_project3) + expect(execute(shared_runner)).to eq(build3_project1) end end context 'shared runner' do - let(:build) { service.execute(shared_runner) } + let(:build) { execute(shared_runner) } it { expect(build).to be_kind_of(Build) } it { expect(build).to be_valid } @@ -122,7 +121,7 @@ module Ci end context 'specific runner' do - let(:build) { service.execute(specific_runner) } + let(:build) { execute(specific_runner) } it { expect(build).to be_kind_of(Build) } it { expect(build).to be_valid } @@ -137,13 +136,13 @@ module Ci end context 'shared runner' do - let(:build) { service.execute(shared_runner) } + let(:build) { execute(shared_runner) } it { expect(build).to be_nil } end context 'specific runner' do - let(:build) { service.execute(specific_runner) } + let(:build) { execute(specific_runner) } it { expect(build).to be_kind_of(Build) } it { expect(build).to be_valid } @@ -159,17 +158,21 @@ module Ci end context 'and uses shared runner' do - let(:build) { service.execute(shared_runner) } + let(:build) { execute(shared_runner) } it { expect(build).to be_nil } end context 'and uses specific runner' do - let(:build) { service.execute(specific_runner) } + let(:build) { execute(specific_runner) } it { expect(build).to be_nil } end end + + def execute(runner) + described_class.new(runner).execute.build + end end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 00d0e20f47c..314ea670a71 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -106,23 +106,46 @@ describe MergeRequests::RefreshService, services: true do context 'push to fork repo source branch' do let(:refresh_service) { service.new(@fork_project, @user) } - before do - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs - end - it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update', @oldrev) + context 'open fork merge request' do + before do + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'executes hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@fork_merge_request, 'update', @oldrev) + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } + it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + context 'closed fork merge request' do + before do + @fork_merge_request.close! + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'do not execute hooks with update action' do + expect(refresh_service).not_to have_received(:execute_hooks) + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_closed } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } + end end context 'push to fork repo target branch' do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index b0cc3ce5f5a..9c92a5080c6 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do context "valid params" do it 'returns a valid note' do - note = Notes::CreateService.new(project, user, opts).execute + note = described_class.new(project, user, opts).execute expect(note).to be_valid end it 'returns a persisted note' do - note = Notes::CreateService.new(project, user, opts).execute + note = described_class.new(project, user, opts).execute expect(note).to be_persisted end it 'note has valid content' do - note = Notes::CreateService.new(project, user, opts).execute + note = described_class.new(project, user, opts).execute expect(note.note).to eq(opts[:note]) end + it 'note belongs to the correct project' do + note = described_class.new(project, user, opts).execute + + expect(note.project).to eq(project) + end + it 'TodoService#new_note is called' do - note = build(:note) - allow(project).to receive_message_chain(:notes, :new).with(opts) { note } + note = build(:note, project: project) + allow(Note).to receive(:new).with(opts) { note } expect_any_instance_of(TodoService).to receive(:new_note).with(note, user) - Notes::CreateService.new(project, user, opts).execute + described_class.new(project, user, opts).execute end it 'enqueues NewNoteWorker' do - note = build(:note, id: 999) - allow(project).to receive_message_chain(:notes, :new).with(opts) { note } + note = build(:note, id: 999, project: project) + allow(Note).to receive(:new).with(opts) { note } expect(NewNoteWorker).to receive(:perform_async).with(note.id) - Notes::CreateService.new(project, user, opts).execute + described_class.new(project, user, opts).execute end end @@ -75,6 +81,27 @@ describe Notes::CreateService, services: true do end end end + + describe 'personal snippet note' do + subject { described_class.new(nil, user, params).execute } + + let(:snippet) { create(:personal_snippet) } + let(:params) do + { note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id } + end + + it 'returns a valid note' do + expect(subject).to be_valid + end + + it 'returns a persisted note' do + expect(subject).to be_persisted + end + + it 'note has valid content' do + expect(subject.note).to eq(params[:note]) + end + end end describe "award emoji" do @@ -88,7 +115,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - note = Notes::CreateService.new(project, user, opts).execute + note = described_class.new(project, user, opts).execute expect(note).to be_valid expect(note.name).to eq('smile') @@ -100,7 +127,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - note = Notes::CreateService.new(project, user, opts).execute + note = described_class.new(project, user, opts).execute expect(note).to be_valid expect(note.note).to eq(opts[:note]) @@ -115,7 +142,7 @@ describe Notes::CreateService, services: true do expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) - Notes::CreateService.new(project, user, opts).execute + described_class.new(project, user, opts).execute end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index f3e80ac22a0..bfbee7ca35f 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -269,6 +269,55 @@ describe NotificationService, services: true do end end + context 'personal snippet note' do + let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) } + let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) } + + before do + @u_watcher = create_global_setting_for(create(:user), :watch) + @u_participant = create_global_setting_for(create(:user), :participating) + @u_disabled = create_global_setting_for(create(:user), :disabled) + @u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention) + @u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention) + @u_note_author = create(:user, username: 'note_author') + @u_snippet_author = create(:user, username: 'snippet_author') + @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating) + + reset_delivered_emails! + end + + let!(:notes) do + [ + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher), + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant), + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned), + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled), + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author), + ] + end + + describe '#new_note' do + it 'notifies the participants' do + notification.new_note(note) + + # it emails participants + should_email(@u_watcher) + should_email(@u_participant) + should_email(@u_watcher) + should_email(@u_snippet_author) + + # it emails mentioned users + should_email(@u_mentioned) + + # it does not email participants with mention notification level + should_not_email(@u_mentioned_level) + + # it does not email note author + should_not_email(@u_note_author) + end + end + end + context 'commit note' do let(:project) { create(:project, :public) } let(:note) { create(:note_on_commit, project: project) } diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb new file mode 100644 index 00000000000..b4efe7de431 --- /dev/null +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe UserProjectAccessChangedService do + describe '#execute' do + it 'schedules the user IDs' do + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait). + with([[1], [2]]) + + described_class.new([1, 2]).execute + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ee3307512d..e160c11111b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,11 +2,11 @@ require './spec/simplecov_env' SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' +ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' -require 'sidekiq/testing/inline' require 'rspec/retry' if ENV['CI'] && !ENV['NO_KNAPSACK'] diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index 49867aa5cc4..a3724b801b3 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do end shared_examples 'an unsubscribeable thread' do + it_behaves_like 'an unsubscribeable thread with incoming address without %{key}' + + it 'has a List-Unsubscribe header in the correct format' do + is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ + is_expected.to have_header 'List-Unsubscribe', /mailto/ + is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/ + end + + it { is_expected.to have_body_text /unsubscribe/ } +end + +shared_examples 'an unsubscribeable thread with incoming address without %{key}' do + include_context 'reply-by-email is enabled with incoming address without %{key}' + it 'has a List-Unsubscribe header in the correct format' do is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.to have_header 'List-Unsubscribe', /^<.+>$/ + is_expected.not_to have_header 'List-Unsubscribe', /mailto/ + is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/ end it { is_expected.to have_body_text /unsubscribe/ } diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb new file mode 100644 index 00000000000..575d3451150 --- /dev/null +++ b/spec/support/sidekiq.rb @@ -0,0 +1,5 @@ +require 'sidekiq/testing/inline' + +Sidekiq::Testing.server_middleware do |chain| + chain.add Gitlab::SidekiqStatus::ServerMiddleware +end diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index ad1c783df4d..1b6c33248c9 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -33,6 +33,30 @@ shared_examples 'a Taskable' do end end + describe 'with nested tasks' do + before do + subject.description = <<-EOT.strip_heredoc + - [ ] Task a + - [x] Task a.1 + - [ ] Task a.2 + - [ ] Task b + + 1. [ ] Task 1 + 1. [ ] Task 1.1 + 1. [ ] Task 1.2 + 1. [x] Task 2 + 1. [x] Task 2.1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('3 of') + expect(subject.task_status).to match('9 tasks completed') + expect(subject.task_status_short).to match('3/') + expect(subject.task_status_short).to match('9 tasks') + end + end + describe 'with an incomplete task' do before do subject.description = <<-EOT.strip_heredoc diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index b6591f272f6..97c4bfcd248 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -3,6 +3,18 @@ require 'spec_helper' describe AuthorizedProjectsWorker do let(:worker) { described_class.new } + describe '.bulk_perform_and_wait' do + it 'schedules the ids and waits for the jobs to complete' do + project = create(:project) + + project.owner.project_authorizations.delete_all + + described_class.bulk_perform_and_wait([[project.owner.id]]) + + expect(project.owner.project_authorizations.count).to eq(1) + end + end + describe '#perform' do it "refreshes user's authorized projects" do user = create(:user) |