diff options
Diffstat (limited to 'spec')
131 files changed, 3503 insertions, 1393 deletions
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 0b8ff006d22..092048a6259 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -1,20 +1,36 @@ require 'spec_helper' describe 'mail_room.yml' do - let(:config_path) { 'config/mail_room.yml' } - let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) } - before(:each) { clear_raw_config } - after(:each) { clear_raw_config } + include StubENV - context 'when incoming email is disabled' do - before do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s - Gitlab::MailRoom.reset_config! - end + let(:mailroom_config_path) { 'config/mail_room.yml' } + let(:gitlab_config_path) { 'config/mail_room.yml' } + let(:redis_config_path) { 'config/resque.yml' } - after do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil - end + let(:configuration) do + vars = { + 'MAIL_ROOM_GITLAB_CONFIG_FILE' => absolute_path(gitlab_config_path), + 'GITLAB_REDIS_CONFIG_FILE' => absolute_path(redis_config_path) + } + cmd = "puts ERB.new(File.read(#{absolute_path(mailroom_config_path).inspect})).result" + + output, status = Gitlab::Popen.popen(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars) + raise "Error interpreting #{mailroom_config_path}: #{output}" unless status.zero? + + YAML.load(output) + end + + before(:each) do + stub_env('GITLAB_REDIS_CONFIG_FILE', absolute_path(redis_config_path)) + clear_redis_raw_config + end + + after(:each) do + clear_redis_raw_config + end + + context 'when incoming email is disabled' do + let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_disabled.yml' } it 'contains no configuration' do expect(configuration[:mailboxes]).to be_nil @@ -22,21 +38,12 @@ describe 'mail_room.yml' do end context 'when incoming email is enabled' do - let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } - let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) } - - before do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s - Gitlab::MailRoom.reset_config! - end + let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' } + let(:redis_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' } - after do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil - end + let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) } it 'contains the intended configuration' do - stub_const('Gitlab::Redis::CONFIG_FILE', redis_config) - expect(configuration[:mailboxes].length).to eq(1) mailbox = configuration[:mailboxes].first @@ -66,9 +73,13 @@ describe 'mail_room.yml' do end end - def clear_raw_config + def clear_redis_raw_config Gitlab::Redis.remove_instance_variable(:@_raw_config) rescue NameError # raised if @_raw_config was not set; ignore end + + def absolute_path(path) + Rails.root.join(path).to_s + end end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 7072bd5e87c..71a4a2c43c7 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -49,4 +49,18 @@ describe Dashboard::TodosController do expect(json_response).to eq({ "count" => "1", "done_count" => "0" }) end end + + describe 'PATCH #bulk_restore' do + let(:todos) { create_list(:todo, 2, :done, user: user, project: project, author: author) } + + it 'restores the todos to pending state' do + patch :bulk_restore, ids: todos.map(&:id) + + todos.each do |todo| + expect(todo.reload).to be_pending + end + expect(response).to have_http_status(200) + expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' }) + end + end end diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb deleted file mode 100644 index 58caf7999cf..00000000000 --- a/spec/controllers/profiles/notifications_controller_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - -describe Profiles::NotificationsController do - let(:user) do - create(:user) do |user| - user.emails.create(email: 'original@example.com') - user.emails.create(email: 'new@example.com') - user.update(notification_email: 'original@example.com') - user.save! - end - end - - describe 'GET show' do - it 'renders' do - sign_in(user) - - get :show - - expect(response).to render_template :show - end - end - - describe 'POST update' do - it 'updates only permitted attributes' do - sign_in(user) - - put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } - - user.reload - expect(user.notification_email).to eq('new@example.com') - expect(user.notified_of_own_activity).to eq(true) - expect(user.admin).to eq(false) - expect(controller).to set_flash[:notice].to('Notification settings saved') - end - - it 'shows an error message if the params are invalid' do - sign_in(user) - - put :update, user: { notification_email: '' } - - expect(user.reload.notification_email).to eq('original@example.com') - expect(controller).to set_flash[:alert].to('Failed to save new settings') - end - end -end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 298a7ff179c..d20e7368086 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -266,5 +266,19 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end + + context 'show_all = true' do + it 'returns all the branches name' do + get :index, + namespace_id: project.namespace, + project_id: project, + format: :json, + show_all: true + + parsed_response = JSON.parse(response.body) + + expect(parsed_response.length).to eq(project.repository.branches.count) + end + end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 46c758b4654..6ceaf96f78f 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -104,7 +104,16 @@ describe Projects::IssuesController do project_with_repository.team << [user, :developer] mr = create(:merge_request_with_diff_notes, source_project: project_with_repository) - get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid + get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid + + expect(assigns(:issue).title).not_to be_empty + expect(assigns(:issue).description).not_to be_empty + end + + it 'fills in an issue for a discussion' do + note = create(:note_on_merge_request, project: project) + + get :new, namespace_id: project.namespace.path, project_id: project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id expect(assigns(:issue).title).not_to be_empty expect(assigns(:issue).description).not_to be_empty @@ -462,11 +471,11 @@ describe Projects::IssuesController do end let(:merge_request_params) do - { merge_request_for_resolving_discussions: merge_request.iid } + { merge_request_to_resolve_discussions_of: merge_request.iid } end - def post_issue(issue_params) - post :create, namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid + def post_issue(issue_params, other_params: {}) + post :create, { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params) end it 'creates an issue for the project' do @@ -485,6 +494,27 @@ describe Projects::IssuesController do expect(discussion.resolved?).to eq(true) end + + it 'sets a flash message' do + post_issue(title: 'Hello') + + expect(flash[:notice]).to eq('Resolved all discussions.') + end + + describe "resolving a single discussion" do + before do + post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id }) + end + it 'resolves a single discussion' do + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + + it 'sets a flash message that one discussion was resolved' do + expect(flash[:notice]).to eq('Resolved 1 discussion.') + end + end end context 'Akismet is enabled' do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 4cebe3884bf..952071af57f 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::RawController do let(:public_project) { create(:project, :public, :repository) } - describe "#show" do + describe '#show' do context 'regular filename' do let(:id) { 'master/README.md' } @@ -16,8 +16,8 @@ describe Projects::RawController do expect(response).to have_http_status(200) expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') expect(response.header['Content-Disposition']). - to eq("inline") - expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") + to eq('inline') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end end @@ -32,7 +32,7 @@ describe Projects::RawController do expect(response).to have_http_status(200) expect(response.header['Content-Type']).to eq('image/jpeg') - expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end end @@ -40,32 +40,57 @@ describe Projects::RawController do let(:id) { 'be93687/files/lfs/lfs_object.iso' } let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') } - context 'when project has access' do + context 'when lfs is enabled' do before do - public_project.lfs_objects << lfs_object - allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.head :ok } + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) end - it 'serves the file' do - expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment') - get(:show, - namespace_id: public_project.namespace.to_param, - project_id: public_project, - id: id) + context 'when project has access' do + before do + public_project.lfs_objects << lfs_object + allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) + allow(controller).to receive(:send_file) { controller.head :ok } + end - expect(response).to have_http_status(200) + it 'serves the file' do + expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment') + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project, + id: id) + + expect(response).to have_http_status(200) + end + end + + context 'when project does not have access' do + it 'does not serve the file' do + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project, + id: id) + + expect(response).to have_http_status(404) + end end end - context 'when project does not have access' do - it 'does not serve the file' do + context 'when lfs is not enabled' do + before do + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false) + end + + it 'delivers ASCII file' do get(:show, namespace_id: public_project.namespace.to_param, project_id: public_project, id: id) - expect(response).to have_http_status(404) + expect(response).to have_http_status(200) + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + expect(response.header['Content-Disposition']). + to eq('inline') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index a1ec41322ad..a88ffc1ea6a 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -78,10 +78,12 @@ describe ProjectsController do it 'shows issues list page if wiki is disabled' do project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + create(:issue, project: project) get :show, namespace_id: project.namespace, id: project expect(response).to render_template('projects/issues/_issues') + expect(assigns(:issuable_meta_data)).not_to be_nil end it 'shows customize workflow page if wiki and issues are disabled' do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 5c50cd7f4ad..fe19a404e16 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -26,12 +26,17 @@ FactoryGirl.define do factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do association :project, :repository + + transient do + line_number 14 + end + position do Gitlab::Diff::Position.new( old_path: "files/ruby/popen.rb", new_path: "files/ruby/popen.rb", old_line: nil, - new_line: 14, + new_line: line_number, diff_refs: noteable.diff_refs ) end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index a3e24bb5ffa..d17a418b8c3 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -51,7 +51,7 @@ describe 'Issue Boards add issue modal', :feature, :js do end it 'does not show tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:title]).not_to eq("Please add a list to your board first") end @@ -107,6 +107,9 @@ describe 'Issue Boards add issue modal', :feature, :js do it 'returns issues' do page.within('.add-issues-modal') do find('.form-control').native.send_keys(issue.title) + find('.form-control').native.send_keys(:enter) + + wait_for_vue_resource expect(page).to have_selector('.card', count: 1) end @@ -115,6 +118,9 @@ describe 'Issue Boards add issue modal', :feature, :js do it 'returns no issues' do page.within('.add-issues-modal') do find('.form-control').native.send_keys('testing search') + find('.form-control').native.send_keys(:enter) + + wait_for_vue_resource expect(page).not_to have_selector('.card') expect(page).not_to have_content("You haven't added any issues to your project yet") diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ecc356f2505..f7e8b78b54d 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -29,7 +29,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'shows tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:"data-original-title"]).to eq("Please add a list to your board first") end @@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search done list' do - page.within('#js-boards-search') do - find('.form-control').set(issue8.title) - end + find('.filtered-search').set(issue8.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search list' do - page.within('#js-boards-search') do - find('.form-control').set(issue5.title) - end + find('.filtered-search').set(issue5.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(find('.issue-boards-search')).to have_selector('.open') + expect(page).to have_css('#js-add-list.open') end it 'creates new list from a new label' do @@ -359,17 +357,9 @@ describe 'Issue Boards', feature: true, js: true do context 'filtering' do it 'filters by author' do - page.within '.issues-filters' do - click_button('Author') - wait_for_ajax - - page.within '.dropdown-menu-author' do - click_link(user2.name) - end - wait_for_vue_resource - - expect(find('.js-author-search')).to have_content(user2.name) - end + set_filter("author", user2.username) + click_filter_link(user2.username) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -377,17 +367,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by assignee' do - page.within '.issues-filters' do - click_button('Assignee') - wait_for_ajax - - page.within '.dropdown-menu-assignee' do - click_link(user.name) - end - wait_for_vue_resource - - expect(find('.js-assignee-search')).to have_content(user.name) - end + set_filter("assignee", user.username) + click_filter_link(user.username) + submit_filter wait_for_vue_resource @@ -396,17 +378,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by milestone' do - page.within '.issues-filters' do - click_button('Milestone') - wait_for_ajax - - page.within '.milestone-filter' do - click_link(milestone.title) - end - wait_for_vue_resource - - expect(find('.js-milestone-select')).to have_content(milestone.title) - end + set_filter("milestone", "\"#{milestone.title}\"") + click_filter_link(milestone.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -415,16 +389,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -432,19 +399,14 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label with space after reload' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(accepting.title) - wait_for_vue_resource(spinner: false) - find('.dropdown-menu-close').click - end - end + set_filter("label", "\"#{accepting.title}\"") + click_filter_link(accepting.title) + submit_filter # Test after reload page.evaluate_script 'window.location.reload()' + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..3)) wait_for_vue_resource @@ -460,26 +422,16 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes filtered labels' do - wait_for_vue_resource + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter - page.within '.labels-filter' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end - - expect(page).to have_css('input[name="label_name[]"]', visible: false) + wait_for_board_cards(1, 1) - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end + find('.clear-search').click + submit_filter - expect(page).not_to have_css('input[name="label_name[]"]', visible: false) - end + wait_for_board_cards(1, 8) end it 'infinite scrolls list with label filter' do @@ -487,16 +439,9 @@ describe 'Issue Boards', feature: true, js: true do create(:labeled_issue, project: project, labels: [planning, testing]) end - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource @@ -518,18 +463,13 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by multiple labels' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax + set_filter("label", testing.title) + click_filter_link(testing.title) - page.within(find('.dropdown-menu-labels')) do - click_link(testing.title) - wait_for_vue_resource - click_link(bug.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", bug.title) + click_filter_link(bug.title) + + submit_filter wait_for_vue_resource @@ -545,14 +485,14 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + page.within('.tokens-container') do + expect(page).to have_content(bug.title) + end + wait_for_vue_resource wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end it 'removes label filter by clicking label button on issue' do @@ -560,16 +500,13 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.card', match: :first)) do click_button(bug.title) end + wait_for_vue_resource expect(page).to have_selector('.card', count: 1) end wait_for_vue_resource - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end end end @@ -643,4 +580,20 @@ describe 'Issue Boards', feature: true, js: true do wait_for_board_cards(board, 0) end end + + def set_filter(type, text) + find('.filtered-search').native.send_keys("#{type}:#{text}") + end + + def submit_filter + find('.filtered-search').native.send_keys(:enter) + end + + def click_filter_link(link_text) + page.within('.filtered-search-input-container') do + expect(page).to have_button(link_text) + + click_button(link_text) + end + end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 1cf0d11d448..e2281a7da55 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe 'Issue Boards add issue modal filtering', :feature, :js do - include WaitForAjax include WaitForVueResource let(:project) { create(:empty_project, :public) } @@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do page.within('.add-issues-modal') do find('.form-control').native.send_keys('testing empty state') + find('.form-control').native.send_keys(:enter) wait_for_vue_resource @@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do it 'restores filters when closing' do visit_board - page.within('.add-issues-modal') do - click_button 'Milestone' - - wait_for_ajax - - click_link 'Upcoming' + set_filter('milestone') + click_filter_link('Upcoming') + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource expect(page).to have_selector('.card', count: 0) @@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do end end - context 'author' do - let!(:issue) { create(:issue, project: project, author: user2) } - - before do - project.team << [user2, :developer] + it 'resotres filters after clicking clear button' do + visit_board - visit_board - end + set_filter('milestone') + click_filter_link('Upcoming') + submit_filter - it 'filters by any author' do - page.within('.add-issues-modal') do - click_button 'Author' + page.within('.add-issues-modal') do + wait_for_vue_resource - wait_for_ajax + expect(page).to have_selector('.card', count: 0) - click_link 'Any Author' + find('.clear-search').click - wait_for_vue_resource + wait_for_vue_resource - expect(page).to have_selector('.card', count: 2) - end + expect(page).to have_selector('.card', count: 1) end + end - it 'filters by selected user' do - page.within('.add-issues-modal') do - click_button 'Author' + context 'author' do + let!(:issue) { create(:issue, project: project, author: user2) } + + before do + project.team << [user2, :developer] - wait_for_ajax + visit_board + end - click_link user2.name + it 'filters by selected user' do + set_filter('author') + click_filter_link(user2.name) + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.card', count: 1) end end @@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do visit_board end - it 'filters by any assignee' do - page.within('.add-issues-modal') do - click_button 'Assignee' - - wait_for_ajax - - click_link 'Any Assignee' - - wait_for_vue_resource - - expect(page).to have_selector('.card', count: 2) - end - end - it 'filters by unassigned' do - page.within('.add-issues-modal') do - click_button 'Assignee' - - wait_for_ajax - - click_link 'Unassigned' + set_filter('assignee') + click_filter_link('No Assignee') + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: 'none') expect(page).to have_selector('.card', count: 1) end end it 'filters by selected user' do - page.within('.add-issues-modal') do - click_button 'Assignee' - - wait_for_ajax - - page.within '.dropdown-menu-user' do - click_link user2.name - end + set_filter('assignee') + click_filter_link(user2.name) + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.card', count: 1) end end @@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do visit_board end - it 'filters by any milestone' do - page.within('.add-issues-modal') do - click_button 'Milestone' - - wait_for_ajax - - click_link 'Any Milestone' - - wait_for_vue_resource - - expect(page).to have_selector('.card', count: 2) - end - end - it 'filters by upcoming milestone' do - page.within('.add-issues-modal') do - click_button 'Milestone' - - wait_for_ajax - - click_link 'Upcoming' + set_filter('milestone') + click_filter_link('Upcoming') + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: 'upcoming') expect(page).to have_selector('.card', count: 0) end end it 'filters by selected milestone' do - page.within('.add-issues-modal') do - click_button 'Milestone' - - wait_for_ajax - - click_link milestone.name + set_filter('milestone') + click_filter_link(milestone.name) + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: milestone.name) expect(page).to have_selector('.card', count: 1) end end @@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do visit_board end - it 'filters by any label' do - page.within('.add-issues-modal') do - click_button 'Label' - - wait_for_ajax - - click_link 'Any Label' - - wait_for_vue_resource - - expect(page).to have_selector('.card', count: 2) - end - end - it 'filters by no label' do - page.within('.add-issues-modal') do - click_button 'Label' - - wait_for_ajax - - click_link 'No Label' + set_filter('label') + click_filter_link('No Label') + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: 'none') expect(page).to have_selector('.card', count: 1) end end it 'filters by label' do - page.within('.add-issues-modal') do - click_button 'Label' - - wait_for_ajax - - click_link label.title + set_filter('label') + click_filter_link(label.title) + submit_filter + page.within('.add-issues-modal') do wait_for_vue_resource + expect(page).to have_selector('.js-visual-token', text: label.title) expect(page).to have_selector('.card', count: 1) end end @@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do click_button('Add issues') end + + def set_filter(type, text = '') + find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}") + end + + def submit_filter + find('.add-issues-modal .filtered-search').native.send_keys(:enter) + end + + def click_filter_link(link_text) + page.within('.add-issues-modal .filtered-search-input-container') do + expect(page).to have_button(link_text) + + click_button(link_text) + end + end end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 4638812b2d9..55df7e45f79 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -2,437 +2,594 @@ require 'spec_helper' describe 'Copy as GFM', feature: true, js: true do include GitlabMarkdownHelper + include RepoHelpers include ActionView::Helpers::JavaScriptHelper before do - @feat = MarkdownFeature.new + login_as :admin + end - # `markdown` helper expects a `@project` variable - @project = @feat.project + describe 'Copying rendered GFM' do + before do + @feat = MarkdownFeature.new - visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) - end + # `markdown` helper expects a `@project` variable + @project = @feat.project - # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. - # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle - # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. + visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) + end - # These are all in a single `it` for performance reasons. - it 'works', :aggregate_failures do - verify( - 'nesting', + # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. + # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM. + # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle + # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. - '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' - ) + # These are all in a single `it` for performance reasons. + it 'works', :aggregate_failures do + verify( + 'nesting', - verify( - 'a real world example from the gitlab-ce README', + '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' + ) - <<-GFM.strip_heredoc - # GitLab + verify( + 'a real world example from the gitlab-ce README', - [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) - [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) - [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) - [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) + <<-GFM.strip_heredoc + # GitLab - ## Canonical source + [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) + [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) + [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) + [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) - The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). + ## Canonical source - ## Open source software to collaborate on code + The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). - To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). + ## Open source software to collaborate on code + To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - - Manage Git repositories with fine grained access controls that keep your code secure - - Perform code reviews and enhance collaboration with merge requests + - Manage Git repositories with fine grained access controls that keep your code secure - - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications + - Perform code reviews and enhance collaboration with merge requests - - Each project can also have an issue tracker, issue board, and a wiki + - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises + - Each project can also have an issue tracker, issue board, and a wiki - - Completely free and open source (MIT Expat license) - GFM - ) + - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - verify( - 'InlineDiffFilter', + - Completely free and open source (MIT Expat license) + GFM + ) - '{-Deleted text-}', - '{+Added text+}' - ) + verify( + 'InlineDiffFilter', - verify( - 'TaskListFilter', + '{-Deleted text-}', + '{+Added text+}' + ) - '- [ ] Unchecked task', - '- [x] Checked task', - '1. [ ] Unchecked numbered task', - '1. [x] Checked numbered task' - ) + verify( + 'TaskListFilter', - verify( - 'ReferenceFilter', + '- [ ] Unchecked task', + '- [x] Checked task', + '1. [ ] Unchecked numbered task', + '1. [x] Checked numbered task' + ) - # issue reference - @feat.issue.to_reference, - # full issue reference - @feat.issue.to_reference(full: true), - # issue URL - namespace_project_issue_url(@project.namespace, @project, @feat.issue), - # issue URL with note anchor - namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), - # issue link - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", - # issue link with note anchor - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", - ) + verify( + 'ReferenceFilter', - verify( - 'AutolinkFilter', + # issue reference + @feat.issue.to_reference, + # full issue reference + @feat.issue.to_reference(full: true), + # issue URL + namespace_project_issue_url(@project.namespace, @project, @feat.issue), + # issue URL with note anchor + namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), + # issue link + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", + # issue link with note anchor + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", + ) - 'https://example.com' - ) + verify( + 'AutolinkFilter', - verify( - 'TableOfContentsFilter', + 'https://example.com' + ) - '[[_TOC_]]' - ) + verify( + 'TableOfContentsFilter', - verify( - 'EmojiFilter', + '[[_TOC_]]' + ) - ':thumbsup:' - ) + verify( + 'EmojiFilter', - verify( - 'ImageLinkFilter', - - '![Image](https://example.com/image.png)' - ) + ':thumbsup:' + ) - verify( - 'VideoLinkFilter', + verify( + 'ImageLinkFilter', + + '![Image](https://example.com/image.png)' + ) - '![Video](https://example.com/video.mp4)' - ) + verify( + 'VideoLinkFilter', - verify( - 'MathFilter: math as converted from GFM to HTML', + '![Video](https://example.com/video.mp4)' + ) - '$`c = \pm\sqrt{a^2 + b^2}`$', + verify( + 'MathFilter: math as converted from GFM to HTML', - # math block - <<-GFM.strip_heredoc - ```math - c = \pm\sqrt{a^2 + b^2} - ``` - GFM - ) + '$`c = \pm\sqrt{a^2 + b^2}`$', - aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do - gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' + # math block + <<-GFM.strip_heredoc + ```math + c = \pm\sqrt{a^2 + b^2} + ``` + GFM + ) - html = <<-HTML.strip_heredoc - <span class="katex"> - <span class="katex-mathml"> - <math> - <semantics> - <mrow> - <mi>c</mi> - <mo>=</mo> - <mo>±</mo> - <msqrt> - <mrow> - <msup> - <mi>a</mi> - <mn>2</mn> - </msup> - <mo>+</mo> - <msup> - <mi>b</mi> - <mn>2</mn> - </msup> - </mrow> - </msqrt> - </mrow> - <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation> - </semantics> - </math> - </span> - <span class="katex-html" aria-hidden="true"> - <span class="strut" style="height: 0.913389em;"></span> - <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span> - <span class="base textstyle uncramped"> - <span class="mord mathit">c</span> - <span class="mrel">=</span> - <span class="mord">±</span> - <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;"> - <span class="style-wrap reset-textstyle textstyle uncramped">√</span> - </span> - <span class="vlist"> - <span class="" style="top: 0em;"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 1em;">​</span> - </span> - <span class="mord textstyle cramped"> - <span class="mord"> - <span class="mord mathit">a</span> - <span class="msupsub"> - <span class="vlist"> - <span class="" style="top: -0.289em; margin-right: 0.05em;"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 0em;">​</span> - </span> - <span class="reset-textstyle scriptstyle cramped"> - <span class="mord mathrm">2</span> + aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do + gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' + + html = <<-HTML.strip_heredoc + <span class="katex"> + <span class="katex-mathml"> + <math> + <semantics> + <mrow> + <mi>c</mi> + <mo>=</mo> + <mo>±</mo> + <msqrt> + <mrow> + <msup> + <mi>a</mi> + <mn>2</mn> + </msup> + <mo>+</mo> + <msup> + <mi>b</mi> + <mn>2</mn> + </msup> + </mrow> + </msqrt> + </mrow> + <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation> + </semantics> + </math> + </span> + <span class="katex-html" aria-hidden="true"> + <span class="strut" style="height: 0.913389em;"></span> + <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span> + <span class="base textstyle uncramped"> + <span class="mord mathit">c</span> + <span class="mrel">=</span> + <span class="mord">±</span> + <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;"> + <span class="style-wrap reset-textstyle textstyle uncramped">√</span> + </span> + <span class="vlist"> + <span class="" style="top: 0em;"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 1em;">​</span> + </span> + <span class="mord textstyle cramped"> + <span class="mord"> + <span class="mord mathit">a</span> + <span class="msupsub"> + <span class="vlist"> + <span class="" style="top: -0.289em; margin-right: 0.05em;"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 0em;">​</span> + </span> + <span class="reset-textstyle scriptstyle cramped"> + <span class="mord mathrm">2</span> + </span> </span> + <span class="baseline-fix"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 0em;">​</span> + </span> + ​</span> </span> - <span class="baseline-fix"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 0em;">​</span> - </span> - ​</span> </span> </span> - </span> - <span class="mbin">+</span> - <span class="mord"> - <span class="mord mathit">b</span> - <span class="msupsub"> - <span class="vlist"> - <span class="" style="top: -0.289em; margin-right: 0.05em;"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 0em;">​</span> - </span> - <span class="reset-textstyle scriptstyle cramped"> - <span class="mord mathrm">2</span> + <span class="mbin">+</span> + <span class="mord"> + <span class="mord mathit">b</span> + <span class="msupsub"> + <span class="vlist"> + <span class="" style="top: -0.289em; margin-right: 0.05em;"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 0em;">​</span> + </span> + <span class="reset-textstyle scriptstyle cramped"> + <span class="mord mathrm">2</span> + </span> </span> + <span class="baseline-fix"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 0em;">​</span> + </span> + ​</span> </span> - <span class="baseline-fix"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 0em;">​</span> - </span> - ​</span> </span> </span> </span> </span> - </span> - <span class="" style="top: -0.833389em;"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 1em;">​</span> + <span class="" style="top: -0.833389em;"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 1em;">​</span> + </span> + <span class="reset-textstyle textstyle uncramped sqrt-line"></span> </span> - <span class="reset-textstyle textstyle uncramped sqrt-line"></span> + <span class="baseline-fix"> + <span class="fontsize-ensurer reset-size5 size5"> + <span class="" style="font-size: 1em;">​</span> + </span> + ​</span> </span> - <span class="baseline-fix"> - <span class="fontsize-ensurer reset-size5 size5"> - <span class="" style="font-size: 1em;">​</span> - </span> - ​</span> </span> </span> </span> </span> - </span> - HTML + HTML - output_gfm = html_to_gfm(html) - expect(output_gfm.strip).to eq(gfm.strip) - end + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end - verify( - 'SanitizationFilter', + verify( + 'SanitizationFilter', - <<-GFM.strip_heredoc - <a name="named-anchor"></a> + <<-GFM.strip_heredoc + <a name="named-anchor"></a> - <sub>sub</sub> + <sub>sub</sub> - <dl> - <dt>dt</dt> - <dd>dd</dd> - </dl> + <dl> + <dt>dt</dt> + <dd>dd</dd> + </dl> - <kbd>kbd</kbd> + <kbd>kbd</kbd> - <q>q</q> + <q>q</q> - <samp>samp</samp> + <samp>samp</samp> - <var>var</var> + <var>var</var> - <ruby>ruby</ruby> + <ruby>ruby</ruby> - <rt>rt</rt> + <rt>rt</rt> - <rp>rp</rp> + <rp>rp</rp> - <abbr>abbr</abbr> + <abbr>abbr</abbr> - <summary>summary</summary> + <summary>summary</summary> - <details>details</details> - GFM - ) + <details>details</details> + GFM + ) - verify( - 'SanitizationFilter', + verify( + 'SanitizationFilter', - <<-GFM.strip_heredoc, - ``` - Plain text - ``` - GFM + <<-GFM.strip_heredoc, + ``` + Plain text + ``` + GFM - <<-GFM.strip_heredoc, - ```ruby - def foo - bar - end - ``` - GFM + <<-GFM.strip_heredoc, + ```ruby + def foo + bar + end + ``` + GFM + + <<-GFM.strip_heredoc + Foo + + This is an example of GFM - <<-GFM.strip_heredoc - Foo + ```js + Code goes here + ``` + GFM + ) - This is an example of GFM + verify( + 'MarkdownFilter', - ```js - Code goes here - ``` - GFM - ) + "Line with two spaces at the end \nto insert a linebreak", - verify( - 'MarkdownFilter', + '`code`', + '`` code with ` ticks ``', - "Line with two spaces at the end \nto insert a linebreak", + '> Quote', - '`code`', - '`` code with ` ticks ``', + # multiline quote + <<-GFM.strip_heredoc, + > Multiline + > Quote + > + > With multiple paragraphs + GFM - '> Quote', + '![Image](https://example.com/image.png)', - # multiline quote - <<-GFM.strip_heredoc, - > Multiline - > Quote - > - > With multiple paragraphs - GFM + '# Heading with no anchor link', - '![Image](https://example.com/image.png)', + '[Link](https://example.com)', - '# Heading with no anchor link', + '- List item', - '[Link](https://example.com)', + # multiline list item + <<-GFM.strip_heredoc, + - Multiline + List item + GFM - '- List item', + # nested lists + <<-GFM.strip_heredoc, + - Nested - # multiline list item - <<-GFM.strip_heredoc, - - Multiline - List item - GFM - # nested lists - <<-GFM.strip_heredoc, - - Nested + - Lists + GFM + # list with blockquote + <<-GFM.strip_heredoc, + - List - - Lists - GFM + > Blockquote + GFM - # list with blockquote - <<-GFM.strip_heredoc, - - List + '1. Numbered list item', - > Blockquote - GFM + # multiline numbered list item + <<-GFM.strip_heredoc, + 1. Multiline + Numbered list item + GFM - '1. Numbered list item', + # nested numbered list + <<-GFM.strip_heredoc, + 1. Nested - # multiline numbered list item - <<-GFM.strip_heredoc, - 1. Multiline - Numbered list item - GFM - # nested numbered list - <<-GFM.strip_heredoc, - 1. Nested + 1. Numbered lists + GFM + '# Heading', + '## Heading', + '### Heading', + '#### Heading', + '##### Heading', + '###### Heading', - 1. Numbered lists - GFM + '**Bold**', - '# Heading', - '## Heading', - '### Heading', - '#### Heading', - '##### Heading', - '###### Heading', + '_Italics_', - '**Bold**', + '~~Strikethrough~~', - '_Italics_', + '2^2', - '~~Strikethrough~~', + '-----', - '2^2', + # table + <<-GFM.strip_heredoc, + | Centered | Right | Left | + |:--------:|------:|------| + | Foo | Bar | **Baz** | + | Foo | Bar | **Baz** | + GFM - '-----', + # table with empty heading + <<-GFM.strip_heredoc, + | | x | y | + |---|---|---| + | a | 1 | 0 | + | b | 0 | 1 | + GFM + ) + end + + alias_method :gfm_to_html, :markdown - # table - <<-GFM.strip_heredoc, - | Centered | Right | Left | - |:--------:|------:|------| - | Foo | Bar | **Baz** | - | Foo | Bar | **Baz** | - GFM + def verify(label, *gfms) + aggregate_failures(label) do + gfms.each do |gfm| + html = gfm_to_html(gfm) + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + end + end - # table with empty heading - <<-GFM.strip_heredoc, - | | x | y | - |---|---|---| - | a | 1 | 0 | - | b | 0 | 1 | - GFM - ) + # Fake a `current_user` helper + def current_user + @feat.user + end end - alias_method :gfm_to_html, :markdown + describe 'Copying code' do + let(:project) { create(:project) } + + context 'from a diff' do + before do + visit namespace_project_commit_path(project.namespace, project, sample_commit.id) + end + + context 'selecting one word of text' do + it 'copies as inline code' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no', - def html_to_gfm(html) + '`RuntimeError`' + ) + end + end + + context 'selecting one line of text' do + it 'copies as inline code' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line', + + '`raise RuntimeError, "System commands must be given as an array of strings"`' + ) + end + end + + context 'selecting multiple lines of text' do + it 'copies as a code block' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', + + <<-GFM.strip_heredoc, + ```ruby + raise RuntimeError, "System commands must be given as an array of strings" + end + ``` + GFM + ) + end + end + end + + context 'from a blob' do + before do + visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb')) + end + + context 'selecting one word of text' do + it 'copies as inline code' do + verify( + '.line[id="LC9"] .no', + + '`RuntimeError`' + ) + end + end + + context 'selecting one line of text' do + it 'copies as inline code' do + verify( + '.line[id="LC9"]', + + '`raise RuntimeError, "System commands must be given as an array of strings"`' + ) + end + end + + context 'selecting multiple lines of text' do + it 'copies as a code block' do + verify( + '.line[id="LC9"], .line[id="LC10"]', + + <<-GFM.strip_heredoc, + ```ruby + raise RuntimeError, "System commands must be given as an array of strings" + end + ``` + GFM + ) + end + end + end + + context 'from a GFM code block' do + before do + visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md')) + end + + context 'selecting one word of text' do + it 'copies as inline code' do + verify( + '.line[id="LC27"] .s2', + + '`"bio"`' + ) + end + end + + context 'selecting one line of text' do + it 'copies as inline code' do + verify( + '.line[id="LC27"]', + + '`"bio": null,`' + ) + end + end + + context 'selecting multiple lines of text' do + it 'copies as a code block with the correct language' do + verify( + '.line[id="LC27"], .line[id="LC28"]', + + <<-GFM.strip_heredoc, + ```json + "bio": null, + "skype": "", + ``` + GFM + ) + end + end + end + + def verify(selector, gfm) + html = html_for_selector(selector) + output_gfm = html_to_gfm(html, 'transformCodeSelection') + expect(output_gfm.strip).to eq(gfm.strip) + end + end + + def html_for_selector(selector) + js = <<-JS.strip_heredoc + (function(selector) { + var els = document.querySelectorAll(selector); + var htmls = _.map(els, function(el) { return el.outerHTML; }); + return htmls.join("\\n"); + })("#{escape_javascript(selector)}") + JS + page.evaluate_script(js) + end + + def html_to_gfm(html, transformer = 'transformGFMSelection') js = <<-JS.strip_heredoc (function(html) { + var transformer = window.gl.CopyAsGFM[#{transformer.inspect}]; + var node = document.createElement('div'); node.innerHTML = html; + + node = transformer(node); + if (!node) return null; + return window.gl.CopyAsGFM.nodeToGFM(node); })("#{escape_javascript(html)}") JS page.evaluate_script(js) end - - def verify(label, *gfms) - aggregate_failures(label) do - gfms.each do |gfm| - html = gfm_to_html(gfm) - output_gfm = html_to_gfm(html) - expect(output_gfm.strip).to eq(gfm.strip) - end - end - end - - # Fake a `current_user` helper - def current_user - @feat.user - end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 63eb5c697c2..c4e58d14f75 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -1,10 +1,32 @@ require 'spec_helper' RSpec.describe 'Dashboard Projects', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, name: "awesome stuff") } + before do - login_as(create :user) + project.team << [user, :developer] + login_as user visit dashboard_projects_path end - + + it 'shows the project the user in a member of in the list' do + visit dashboard_projects_path + expect(page).to have_content('awesome stuff') + end + + describe "with a pipeline" do + let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } + + before do + pipeline + end + + it 'shows that the last pipeline passed' do + visit dashboard_projects_path + expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']") + end + end + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" end diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle.rb new file mode 100644 index 00000000000..ada4ac66e04 --- /dev/null +++ b/spec/features/groups/group_name_toggle.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Group name toggle', js: true do + let(:group) { create(:group) } + let(:nested_group_1) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group_1) } + let(:nested_group_3) { create(:group, parent: nested_group_2) } + + before do + login_as :user + end + + it 'is not present for less than 3 groups' do + visit group_path(group) + expect(page).not_to have_css('.group-name-toggle') + + visit group_path(nested_group_1) + expect(page).not_to have_css('.group-name-toggle') + end + + it 'is present for nested group of 3 or more in the namespace' do + visit group_path(nested_group_2) + expect(page).to have_css('.group-name-toggle') + + visit group_path(nested_group_3) + expect(page).to have_css('.group-name-toggle') + end + + context 'for group with at least 3 groups' do + before do + visit group_path(nested_group_2) + end + + it 'should show the full group namespace when toggled' do + expect(page).not_to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: false) + + click_button '...' + + expect(page).to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: true) + end + end +end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index 73553f97d6f..bfe43bff10f 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do end def selected_sort_order - find('.pull-right .dropdown button').text.downcase + find('.filter-dropdown-container .dropdown button').text.downcase end def visit_merge_requests_with_state(project, state) diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index f424186cf30..16e453bc328 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -17,8 +17,21 @@ describe 'Awards Emoji', feature: true do login_as(user) end + describe 'visiting an issue with a legacy award emoji that is not valid anymore' do + before do + # The `heart_tip` emoji is not valid anymore so we need to skip validation + issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 + it 'does not shows a 500 page' do + expect(page).to have_text(issue.title) + end + end + describe 'Click award emoji from issue#show' do - let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } before do visit namespace_project_issue_path(project.namespace, project, issue) diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index 762cab0c0e1..572bca3de21 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -1,76 +1,93 @@ require 'rails_helper' -feature 'Resolving all open discussions in a merge request from an issue', feature: true do +feature 'Resolving all open discussions in a merge request from an issue', feature: true, js: true do let(:user) { create(:user) } - let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) } + let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first } - before do - project.team << [user, :master] - login_as user - end - - context 'with the internal tracker disabled' do + describe 'as a user with access to the project' do before do - project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + project.team << [user, :master] + login_as user visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'does not show a link to create a new issue' do - expect(page).not_to have_link 'open an issue to resolve them later' - end - end - - context 'merge request has discussions that need to be resolved' do - before do - visit namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'shows a button to resolve all discussions by creating a new issue' do + within('li#resolve-count-app') do + expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) + end end - it 'shows a warning that the merge request contains unresolved discussions' do - expect(page).to have_content 'This merge request has unresolved discussions' - end + context 'resolving the discussion' do + before do + click_button 'Resolve discussion' + end - it 'has a link to resolve all discussions by creating an issue' do - page.within '.mr-widget-body' do - expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + it 'hides the link for creating a new issue' do + expect(page).not_to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) end end context 'creating an issue for discussions' do before do - page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + click_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) end - it 'shows an issue with the title filled in' do - title_field = page.find_field('issue[title]') + it_behaves_like 'creating an issue for a discussion' + end - expect(title_field.value).to include(merge_request.title) + context 'for a project where all discussions need to be resolved before merging' do + before do + project.update_attribute(:only_allow_merge_if_all_discussions_are_resolved, true) end - it 'has a mention of the discussion in the description' do - description_field = page.find_field('issue[description]') + context 'with the internal tracker disabled' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end - expect(description_field.value).to include(discussion.first_note.note) + it 'does not show a link to create a new issue' do + expect(page).not_to have_link 'open an issue to resolve them later' + end end - it 'has a hidden field for the merge request' do - merge_request_field = find('#merge_request_for_resolving_discussions', visible: false) - - expect(merge_request_field.value).to eq(merge_request.iid.to_s) - end + context 'merge request has discussions that need to be resolved' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end - it 'can create a new issue for the project' do - expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1) - end + it 'shows a warning that the merge request contains unresolved discussions' do + expect(page).to have_content 'This merge request has unresolved discussions' + end - it 'resolves the discussion in the merge request' do - click_button 'Submit issue' + it 'has a link to resolve all discussions by creating an issue' do + page.within '.mr-widget-body' do + expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) + end + end - discussion.first_note.reload + context 'creating an issue for discussions' do + before do + page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) + end - expect(discussion.resolved?).to eq(true) + it_behaves_like 'creating an issue for a discussion' + end end end end + + describe 'as a reporter' do + before do + project.team << [user, :reporter] + login_as user + visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) + end + + it 'Shows a notice to ask someone else to resolve the discussions' do + expect(page).to have_content("The discussions at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.") + end + end end diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb new file mode 100644 index 00000000000..88e2cc60d79 --- /dev/null +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +feature 'Resolve an open discussion in a merge request by creating an issue', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) } + let(:merge_request) { create(:merge_request, source_project: project) } + let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first } + + describe 'As a user with access to the project' do + before do + project.team << [user, :master] + login_as user + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'with the internal tracker disabled' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not show a link to create a new issue' do + expect(page).not_to have_link 'Resolve this discussion in a new issue' + end + end + + context 'resolving the discussion', js: true do + before do + click_button 'Resolve discussion' + end + + it 'hides the link for creating a new issue' do + expect(page).not_to have_link 'Resolve this discussion in a new issue' + end + + it 'shows the link for creating a new issue when unresolving a discussion' do + page.within '.diff-content' do + click_button 'Unresolve discussion' + end + + expect(page).to have_link 'Resolve this discussion in a new issue' + end + end + + it 'has a link to create a new issue for a discussion' do + new_issue_link = new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid) + + expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link + end + + context 'creating the issue' do + before do + click_link 'Resolve this discussion in a new issue', href: new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid) + end + + it 'has a hidden field for the discussion' do + discussion_field = find('#discussion_to_resolve', visible: false) + + expect(discussion_field.value).to eq(discussion.id.to_s) + end + + it_behaves_like 'creating an issue for a discussion' + end + end + + describe 'as a reporter' do + before do + project.team << [user, :reporter] + login_as user + visit new_namespace_project_issue_path(project.namespace, project, + merge_request_to_resolve_discussions_of: merge_request.iid, + discussion_to_resolve: discussion.id) + end + + it 'Shows a notice to ask someone else to resolve the discussions' do + expect(page).to have_content("The discussion at #{merge_request.to_reference}"\ + "(discussion #{discussion.first_note.id}) will stay unresolved."\ + "Ask someone with permission to resolve it.") + end + 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 19a00618b12..1772a120045 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -14,9 +14,10 @@ describe 'Dropdown author', js: true, feature: true do def send_keys_to_filtered_search(input) input.split("").each do |i| filtered_search.send_keys(i) - sleep 5 - wait_for_ajax end + + sleep 0.5 + wait_for_ajax end def dropdown_author_size diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 85ffffe4b6d..ce96a420699 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -202,6 +202,14 @@ describe 'Dropdown milestone', :feature, :js do expect_tokens([{ name: 'milestone', value: 'upcoming' }]) expect_filtered_search_input_empty end + + it 'selects `started milestones`' do + click_static_milestone('Started') + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_filtered_search_input_empty + end end describe 'input has existing content' do diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index f079a9627e4..f463312bf57 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do let!(:project) { create(:project, group: group) } let!(:user) { create(:user) } let!(:user2) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } - let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } @@ -505,6 +504,14 @@ describe 'Filter issues', js: true, feature: true do expect_filtered_search_input_empty end + it 'filters issues by started milestones' do + input_filtered_search("milestone:started") + + expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_issues_list_count(5) + expect_filtered_search_input_empty + end + it 'filters issues by invalid milestones' do skip('to be tested, issue #26546') end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index d4e0ef91856..755992069ff 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project) } let!(:user) { create(:user)} let!(:user2) { create(:user)} @@ -78,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + issue = Issue.find_by(title: 'title') + + expect(page).to have_text("Issue #{issue.to_reference}") + # compare paths because the host differ in test + expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + end end it 'correctly updates the dropdown toggle when removing a label' do diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index ae609160e18..f32d1f78b40 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -48,6 +48,18 @@ feature 'Login', feature: true do end end + describe 'with the ghost user' do + it 'disallows login' do + login_with(User.ghost) + + expect(page).to have_content('Invalid Login or password.') + end + + it 'does not update Devise trackable attributes' do + expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count } + end + end + describe 'with two-factor authentication' do def enter_code(code) fill_in 'user_otp_attempt', with: code diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1ecdb8b5983..f8518f450dc 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit merge request', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:fork_project) { create(:project, forked_from_project: project) } let!(:user) { create(:user)} @@ -84,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + merge_request = MergeRequest.find_by(source_branch: 'fix') + + expect(page).to have_text("Merge Request #{merge_request.to_reference}") + # compare paths because the host differ in test + expect(find_link(merge_request.to_reference)[:href]) + .to end_with(merge_request_path(merge_request)) + end end end diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb deleted file mode 100644 index e05fbb3715c..00000000000 --- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do - let(:user) { create(:user) } - - before do - login_as(user) - end - - scenario 'User opts into receiving notifications about their own activity' do - visit profile_notifications_path - - expect(page).not_to have_checked_field('user[notified_of_own_activity]') - - check 'user[notified_of_own_activity]' - - expect(page).to have_content('Notification settings saved') - expect(page).to have_checked_field('user[notified_of_own_activity]') - end - - scenario 'User opts out of receiving notifications about their own activity' do - user.update!(notified_of_own_activity: true) - visit profile_notifications_path - - expect(page).to have_checked_field('user[notified_of_own_activity]') - - uncheck 'user[notified_of_own_activity]' - - expect(page).to have_content('Notification settings saved') - expect(page).not_to have_checked_field('user[notified_of_own_activity]') - end -end diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb new file mode 100644 index 00000000000..d94204230f6 --- /dev/null +++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, js: true do + include TreeHelper + + let(:project) { create(:project, :public, :repository) } + let(:path) { 'CHANGELOG' } + let(:sha) { project.repository.commit.sha } + + describe 'On a file(blob)' do + def get_absolute_url(path = "") + "http://#{page.server.host}:#{page.server.port}#{path}" + end + + def visit_blob(fragment = nil) + visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment) + end + + describe 'Click "Permalink" button' do + it 'works with no initial line number fragment hash' do + visit_blob + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path)))) + end + + it 'maintains intitial fragment hash' do + fragment = "L3" + + visit_blob(fragment) + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment))) + end + + it 'changes fragment hash if line number clicked' do + ending_fragment = "L5" + + visit_blob + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment))) + end + + it 'with initial fragment hash, changes fragment hash if line number clicked' do + fragment = "L1" + ending_fragment = "L5" + + visit_blob(fragment) + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment))) + end + end + + describe 'Click "Blame" button' do + it 'works with no initial line number fragment hash' do + visit_blob + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path)))) + end + + it 'maintains intitial fragment hash' do + fragment = "L3" + + visit_blob(fragment) + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: fragment))) + end + + it 'changes fragment hash if line number clicked' do + ending_fragment = "L5" + + visit_blob + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment))) + end + + it 'with initial fragment hash, changes fragment hash if line number clicked' do + fragment = "L1" + ending_fragment = "L5" + + visit_blob(fragment) + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment))) + end + end + end +end diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb new file mode 100644 index 00000000000..03d08c12612 --- /dev/null +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +feature 'New blob creation', feature: true, js: true do + include WaitForAjax + + given(:user) { create(:user) } + given(:role) { :developer } + given(:project) { create(:project) } + given(:content) { 'class NextFeature\nend\n' } + + background do + login_as(user) + project.team << [user, role] + visit namespace_project_new_blob_path(project.namespace, project, 'master') + end + + def edit_file + wait_for_ajax + fill_in 'file_name', with: 'feature.rb' + execute_script("ace.edit('editor').setValue('#{content}')") + end + + def select_branch_index(index) + first('button.js-target-branch').click + wait_for_ajax + all('a[data-group="Branches"]')[index].click + end + + def create_new_branch(name) + first('button.js-target-branch').click + click_link 'Create new branch' + fill_in 'new_branch_name', with: name + click_button 'Create' + end + + def commit_file + click_button 'Commit Changes' + end + + context 'with default target branch' do + background do + edit_file + commit_file + end + + scenario 'creates the blob in the default branch' do + expect(page).to have_content 'master' + expect(page).to have_content 'successfully created' + expect(page).to have_content 'NextFeature' + end + end + + context 'with different target branch' do + background do + edit_file + select_branch_index(0) + commit_file + end + + scenario 'creates the blob in the different branch' do + expect(page).to have_content 'test' + expect(page).to have_content 'successfully created' + end + end + + context 'with a new target branch' do + given(:new_branch_name) { 'new-feature' } + + background do + edit_file + create_new_branch(new_branch_name) + commit_file + end + + scenario 'creates the blob in the new branch' do + expect(page).to have_content new_branch_name + expect(page).to have_content 'successfully created' + end + scenario 'returns you to the mr' do + expect(page).to have_content 'New Merge Request' + expect(page).to have_content "From #{new_branch_name} into master" + expect(page).to have_content 'Add new file' + end + end + + context 'the file already exist in the source branch' do + background do + Files::CreateService.new( + project, + user, + start_branch: 'master', + target_branch: 'master', + commit_message: 'Create file', + file_path: 'feature.rb', + file_content: content + ).execute + edit_file + commit_file + end + + scenario 'shows error message' do + expect(page).to have_content('Your changes could not be committed because a file with the same name already exists') + expect(page).to have_content('New File') + expect(page).to have_content('NextFeature') + end + end +end diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb new file mode 100644 index 00000000000..30a2b2bcf8c --- /dev/null +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +feature 'Mini Pipeline Graph in Commit View', :js, :feature do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + login_as(user) + end + + context 'when commit has pipelines' do + let(:pipeline) do + create(:ci_empty_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end + + let(:build) do + create(:ci_build, pipeline: pipeline) + end + + before do + build.run + visit namespace_project_commit_path(project.namespace, project, project.commit.id) + end + + it 'should display a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') + end + + it 'should show the builds list when stage is clicked' do + first('.mini-pipeline-graph-dropdown-toggle').click + + wait_for_ajax + + page.within '.js-builds-dropdown-list' do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content(build.stage) + end + end + end + + context 'when commit does not have pipelines' do + before do + visit namespace_project_commit_path(project.namespace, project, project.commit.id) + end + + it 'should not display a mini pipeline graph' do + expect(page).not_to have_selector('.mr-widget-pipeline-graph') + end + end +end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 25f31b423b8..641e2cf7402 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -111,10 +111,8 @@ feature 'Environments page', :feature, :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { click_link(action.name.humanize) } + expect { find('.js-manual-action-link').click } .not_to change { Ci::Pipeline.count } - - expect(action.reload).to be_pending end scenario 'does show build name and id' do @@ -158,12 +156,6 @@ feature 'Environments page', :feature, :js do expect(page).to have_selector('.stop-env-link') end - scenario 'starts build when stop button clicked' do - find('.stop-env-link').click - - expect(page).to have_content('close_app') - end - context 'for reporter' do let(:role) { :reporter } diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index 69295e450d0..d281043caa3 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'user checks git blame', feature: true do +feature 'user browses project', feature: true do let(:project) { create(:project) } let(:user) { create(:user) } @@ -18,4 +18,16 @@ feature 'user checks git blame', feature: true do expect(page).to have_content "Dmitriy Zaporozhets" expect(page).to have_content "Initial commit" end + + scenario 'can see raw content of LFS pointer with LFS disabled' do + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false) + click_link 'files' + click_link 'lfs' + click_link 'lfs_object.iso' + + expect(page).not_to have_content 'Download (1.5 MB)' + expect(page).to have_content 'version https://git-lfs.github.com/spec/v1' + expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' + expect(page).to have_content 'size 1575078' + end end diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index de3c6eceb82..e2911a37e40 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -29,7 +29,7 @@ feature 'Issue prioritization', feature: true do issue_1.labels << label_5 login_as user - visit namespace_project_issues_path(project.namespace, project, sort: 'priority') + visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority') # Ensure we are indicating that issues are sorted by priority expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') @@ -68,7 +68,7 @@ feature 'Issue prioritization', feature: true do issue_6.labels << label_5 # 8 - No priority login_as user - visit namespace_project_issues_path(project.namespace, project, sort: 'priority') + visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority') expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 45185f2dd1f..52196ce49bd 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -16,6 +16,15 @@ feature "New project", feature: true do expect(find_field("project_visibility_level_#{level}")).to be_checked end + + it 'saves visibility level on validation error' do + visit new_project_path + + choose(key) + click_button('Create project') + + expect(find_field("project_visibility_level_#{level}")).to be_checked + end end end diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb new file mode 100644 index 00000000000..c17e06612de --- /dev/null +++ b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Projects > Wiki > User views the wiki page', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:old_page_version_id) { wiki_page.versions.last.id } + let(:wiki_page) do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + background do + project.team << [user, :master] + login_as(user) + WikiPages::UpdateService.new( + project, + user, + message: 'updated home', + content: 'updated [some link](other-page)', + format: :markdown + ).execute(wiki_page) + end + + scenario 'Visit Wiki Page Current Commit' do + visit namespace_project_wiki_path(project.namespace, project, wiki_page) + + expect(page).to have_selector('a.btn', text: 'Edit') + end + + scenario 'Visit Wiki Page Historical Commit' do + visit namespace_project_wiki_path( + project.namespace, + project, + wiki_page, + version_id: old_page_version_id + ) + + expect(page).not_to have_selector('a.btn', text: 'Edit') + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 3a1240f95b5..ba56030e28d 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -56,7 +56,7 @@ feature 'Project', feature: true do end describe 'removal', js: true do - let(:user) { create(:user) } + let(:user) { create(:user, username: 'test', name: 'test') } let(:project) { create(:project, namespace: user.namespace, name: 'project1') } before do @@ -67,7 +67,7 @@ feature 'Project', feature: true do it 'removes a project' do expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) - expect(page).to have_content "Project 'project1' will be deleted." + expect(page).to have_content "Project 'test / project1' will be deleted." expect(Project.all.count).to be_zero expect(project.issues).to be_empty expect(project.merge_requests).to be_empty diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index 0f30f562539..ccfafe6db7d 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -10,16 +10,12 @@ feature 'Master deletes tag', feature: true do visit namespace_project_tags_path(project.namespace, project) end - context 'from the tags list page' do + context 'from the tags list page', js: true do scenario 'deletes the tag' do expect(page).to have_content 'v1.1.0' - page.within('.content') do - first('.btn-remove').click - end + delete_first_tag - expect(current_path).to eq( - namespace_project_tags_path(project.namespace, project)) expect(page).not_to have_content 'v1.1.0' end end @@ -37,4 +33,23 @@ feature 'Master deletes tag', feature: true do expect(page).not_to have_content 'v1.0.0' end end + + context 'when pre-receive hook fails', js: true do + before do + allow_any_instance_of(GitHooksService).to receive(:execute) + .and_raise(GitHooksService::PreReceiveError, 'Do not delete tags') + end + + scenario 'shows the error message' do + delete_first_tag + + expect(page).to have_content('Do not delete tags') + end + end + + def delete_first_tag + page.within('.content') do + first('.btn-remove').click + end + end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 3495091a0d5..850020109d4 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -31,14 +31,16 @@ describe 'Dashboard Todos', feature: true do end it 'shows due date as today' do - page.within first('.todo') do + within first('.todo') do expect(page).to have_content 'Due today' end end shared_examples 'deleting the todo' do before do - first('.js-done-todo').click + within first('.todo') do + click_link 'Done' + end end it 'is marked as done-reversible in the list' do @@ -62,9 +64,11 @@ describe 'Dashboard Todos', feature: true do shared_examples 'deleting and restoring the todo' do before do - first('.js-done-todo').click - wait_for_ajax - first('.js-undo-todo').click + within first('.todo') do + click_link 'Done' + wait_for_ajax + click_link 'Undo' + end end it 'is marked back as pending in the list' do @@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do end end + context 'User has done todos', js: true do + before do + create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path(state: :done) + end + + it 'has the done todo present' do + expect(page).to have_selector('.todos-list .todo.todo-done', count: 1) + end + + describe 'restoring the todo' do + before do + within first('.todo') do + click_link 'Add todo' + end + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-done') + end + + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' + end + end + end + context 'User has Todos with labels spanning multiple projects' do before do label1 = create(:label, project: project) @@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do describe 'mark all as done', js: true do before do visit dashboard_todos_path - click_link('Mark all as done') + click_link 'Mark all as done' end it 'shows "All done" message!' do @@ -151,6 +184,60 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') end + + it 'shows "Undo mark all as done" button' do + expect(page).to have_selector('.js-todos-mark-all', visible: false) + expect(page).to have_selector('.js-todos-undo-all', visible: true) + end + end + + describe 'undo mark all as done', js: true do + before do + visit dashboard_todos_path + end + + it 'shows the restored todo list' do + mark_all_and_undo + + expect(page).to have_selector('.todos-list .todo', count: 1) + expect(page).to have_selector('.gl-pagination') + expect(page).not_to have_content "You're all done!" + end + + it 'updates todo count' do + mark_all_and_undo + + expect(page).to have_content 'To do 2' + expect(page).to have_content 'Done 0' + end + + it 'shows "Mark all as done" button' do + mark_all_and_undo + + expect(page).to have_selector('.js-todos-mark-all', visible: true) + expect(page).to have_selector('.js-todos-undo-all', visible: false) + end + + context 'User has deleted a todo' do + before do + within first('.todo') do + click_link 'Done' + end + end + + it 'shows the restored todo list with the deleted todo' do + mark_all_and_undo + + expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1) + end + end + + def mark_all_and_undo + click_link 'Mark all as done' + wait_for_ajax + click_link 'Undo mark all as done' + wait_for_ajax + end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 2a008427478..ee52dc65175 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -101,6 +101,41 @@ describe IssuesFinder do end end + context 'filtering by started milestone' do + let(:params) { { milestone_title: Milestone::Started.name } } + + let(:project_no_started_milestones) { create(:empty_project, :public) } + let(:project_started_1_and_2) { create(:empty_project, :public) } + let(:project_started_8) { create(:empty_project, :public) } + + let(:yesterday) { Date.today - 1.day } + let(:tomorrow) { Date.today + 1.day } + let(:two_days_ago) { Date.today - 2.days } + + let(:milestones) do + [ + create(:milestone, project: project_no_started_milestones, start_date: tomorrow), + create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago), + create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday), + create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow), + create(:milestone, project: project_started_8, title: '7.0'), + create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday), + create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow) + ] + end + + before do + milestones.each do |milestone| + create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user) + end + end + + it 'returns issues in the started milestones for each project' do + expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0') + expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday) + end + end + context 'filtering by label' do let(:params) { { label_name: label.title } } diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index fa516f9903e..bead7948486 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -19,12 +19,12 @@ describe BlobHelper do describe '#highlight' do it 'returns plaintext for unknown lexer context' do result = helper.highlight(blob_name, no_context_content) - expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>]) + expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>]) end it 'highlights single block' do - expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> -<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] + expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> +<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] expect(helper.highlight(blob_name, blob_content)).to eq(expected) end @@ -43,10 +43,10 @@ describe BlobHelper do let(:blob_name) { 'test.diff' } let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} let(:expected) do - %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span> -<span id="LC2" class="line"><span class="gi">+bbb</span></span> -<span id="LC3" class="line"><span class="gd">- ccc</span></span> -<span id="LC4" class="line"> ddd</span></code></pre>) + %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> +<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> +<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span> +<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>) end it 'highlights each line properly' do diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 637b02d9388..174cc84a97b 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -16,4 +16,11 @@ describe CiStatusHelper do helper.ci_icon_for_status(failed_commit.status) end end + + describe "#pipeline_status_cache_key" do + it "builds a cache key for pipeline status" do + pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success") + expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success") + end + end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 81ba693f2f3..70443d27f33 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -28,7 +28,7 @@ describe EventsHelper do it 'displays the first line of a code block' do input = "```\nCode block\nwith two lines\n```" - expected = %r{<pre.+><code>Code block\.\.\.</code></pre>} + expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>} expect(helper.event_note(input)).to match(expected) end @@ -55,10 +55,8 @@ describe EventsHelper do it 'preserves code color scheme' do input = "```ruby\ndef test\n 'hello world'\nend\n```" expected = '<pre class="code highlight js-syntax-highlight ruby">' \ - "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ - " <span class=\"s1\">\'hello world\'</span>\n" \ - "<span class=\"k\">end</span>\n" \ - '</code></pre>' + "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \ + "</code></pre>" expect(helper.event_note(input)).to eq(expected) end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9ffd4b9371c..6cf3f86680a 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -152,9 +152,8 @@ describe GitlabMarkdownHelper do end describe '#first_line_in_markdown' do - let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} - it 'truncates Markdown properly' do + text = "@#{user.username}, can you look at this?\nHello world\n" actual = first_line_in_markdown(text, 100, project: project) doc = Nokogiri::HTML.parse(actual) @@ -169,6 +168,23 @@ describe GitlabMarkdownHelper do expect(doc.content).to eq "@#{user.username}, can you look at this?..." end + + it 'truncates Markdown with emoji properly' do + text = "foo :wink:\nbar :grinning:" + actual = first_line_in_markdown(text, 100, project: project) + + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + # But also account for the 2 errors caused by the unknown `gl-emoji` elements + expect(doc.errors.length).to eq(2) + + expect(doc.css('gl-emoji').length).to eq(2) + expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink' + expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning' + + expect(doc.content).to eq "foo 😉\nbar 😀" + end end describe '#cross_project_reference' do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 88d853935c7..f0554cc068d 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -131,4 +131,36 @@ describe IssuesHelper do expect(options).to have_selector('option', text: milestone2.title) end end + + describe "#link_to_discussions_to_resolve" do + describe "passing only a merge request" do + let(:merge_request) { create(:merge_request) } + + it "links just the merge request" do + expected_path = namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + + expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path) + end + + it "containst the reference to the merge request" do + expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference) + end + end + + describe "when passing a discussion" do + let(:diff_note) { create(:diff_note_on_merge_request) } + let(:merge_request) { diff_note.noteable } + let(:discussion) { Discussion.new([diff_note]) } + + it "links to the merge request with first note if a single discussion was passed" do + expected_path = Gitlab::UrlBuilder.build(diff_note) + + expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path) + end + + it "contains both the reference to the merge request and a mention of the discussion" do + expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})") + end + end + end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 68b20a1e4fc..77a4ba305bb 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -47,4 +47,54 @@ describe MilestonesHelper do end end end + + describe '#milestone_remaining_days' do + context 'when less than 31 days remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now)) } + + it 'returns days remaining' do + expect(milestone_remaining).to eq("<strong>11</strong> days remaining") + end + end + + context 'when less than 1 year and more than 30 days remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now)) } + + it 'returns months remaining' do + expect(milestone_remaining).to eq("<strong>2</strong> months remaining") + end + end + + context 'when more than 1 year remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 1.year.from_now + 2.days)) } + + it 'returns years remaining' do + expect(milestone_remaining).to eq("<strong>1</strong> year remaining") + end + end + + context 'when milestone is expired' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago)) } + + it 'returns "Past due"' do + expect(milestone_remaining).to eq("<strong>Past due</strong>") + end + end + + context 'when milestone has start_date in the future' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now)) } + + it 'returns "Upcoming"' do + expect(milestone_remaining).to eq("<strong>Upcoming</strong>") + end + end + + context 'when milestone has start_date in the past' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago)) } + + it 'returns days elapsed' do + expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") + end + end + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index aca0bb1d794..fc6ad6419ac 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -63,6 +63,46 @@ describe ProjectsHelper do end end + describe "#project_list_cache_key" do + let(:project) { create(:project) } + + it "includes the namespace" do + expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key) + end + + it "includes the project" do + expect(helper.project_list_cache_key(project)).to include(project.cache_key) + end + + it "includes the controller name" do + expect(helper.controller).to receive(:controller_name).and_return("testcontroller") + + expect(helper.project_list_cache_key(project)).to include("testcontroller") + end + + it "includes the controller action" do + expect(helper.controller).to receive(:action_name).and_return("testaction") + + expect(helper.project_list_cache_key(project)).to include("testaction") + end + + it "includes the application settings" do + settings = Gitlab::CurrentSettings.current_application_settings + + expect(helper.project_list_cache_key(project)).to include(settings.cache_key) + end + + it "includes a version" do + expect(helper.project_list_cache_key(project)).to include("v2.3") + end + + it "includes the pipeline status when there is a status" do + create(:ci_pipeline, :success, project: project, sha: project.commit.sha) + + expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success") + end + end + describe 'link_to_member' do let(:group) { create(:group) } let(:project) { create(:empty_project, group: group) } diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb new file mode 100644 index 00000000000..50060a0925d --- /dev/null +++ b/spec/helpers/todos_helper_spec.rb @@ -0,0 +1,23 @@ +require "spec_helper" + +describe TodosHelper do + describe '#todo_projects_options' do + let(:projects) { create_list(:empty_project, 3) } + let(:user) { create(:user) } + + it 'returns users authorised projects in json format' do + projects.first.add_developer(user) + projects.second.add_developer(user) + + allow(helper).to receive(:current_user).and_return(user) + + expected_results = [ + { 'id' => '', 'text' => 'Any Project' }, + { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace }, + { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace } + ] + + expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results) + end + end +end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 9a2978006aa..0a6e042b700 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,11 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ -import promisePolyfill from 'es6-promise'; import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; -promisePolyfill.polyfill(); - (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js new file mode 100644 index 00000000000..c1179e572ae --- /dev/null +++ b/spec/javascripts/blob/create_branch_dropdown_spec.js @@ -0,0 +1,107 @@ +require('~/gl_dropdown'); +require('~/lib/utils/type_utility'); +require('~/blob/create_branch_dropdown'); +require('~/blob/target_branch_dropdown'); + +describe('CreateBranchDropdown', () => { + const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; + // selectors + const createBranchSel = '.js-new-branch-btn'; + const backBtnSel = '.dropdown-menu-back'; + const cancelBtnSel = '.js-cancel-branch-btn'; + const branchNameSel = '#new_branch_name'; + const branchName = 'new_name'; + let dropdown; + + function createDropdown() { + const dropdownEl = document.querySelector('.js-project-branches-dropdown'); + const projectBranches = getJSONFixture('project_branches.json'); + dropdown = new gl.TargetBranchDropDown(dropdownEl); + dropdown.cachedRefs = projectBranches; + return dropdown; + } + + function createBranchBtn() { + return document.querySelector(createBranchSel); + } + + function backBtn() { + return document.querySelector(backBtnSel); + } + + function cancelBtn() { + return document.querySelector(cancelBtnSel); + } + + function branchNameEl() { + return document.querySelector(branchNameSel); + } + + function changeBranchName(text) { + branchNameEl().value = text; + branchNameEl().dispatchEvent(new Event('change')); + } + + preloadFixtures(fixtureTemplate); + + beforeEach(() => { + loadFixtures(fixtureTemplate); + createDropdown(); + }); + + it('disable submit when branch name is empty', () => { + expect(createBranchBtn()).toBeDisabled(); + }); + + it('enable submit when branch name is present', () => { + changeBranchName(branchName); + + expect(createBranchBtn()).not.toBeDisabled(); + }); + + it('resets the form when cancel btn is clicked and triggers dropdownback', () => { + const spyBackEvent = spyOnEvent(backBtnSel, 'click'); + changeBranchName(branchName); + + cancelBtn().click(); + + expect(branchNameEl()).toHaveValue(''); + expect(spyBackEvent).toHaveBeenTriggered(); + }); + + it('resets the form when back btn is clicked', () => { + changeBranchName(branchName); + + backBtn().click(); + + expect(branchNameEl()).toHaveValue(''); + }); + + describe('new branch creation', () => { + beforeEach(() => { + changeBranchName(branchName); + }); + it('sets the new branch name and updates the dropdown', () => { + spyOn(dropdown, 'setNewBranch'); + + createBranchBtn().click(); + + expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); + }); + + it('resets the form', () => { + createBranchBtn().click(); + + expect(branchNameEl()).toHaveValue(''); + }); + + it('is triggered with enter keypress', () => { + spyOn(dropdown, 'setNewBranch'); + const enterEvent = new Event('keydown'); + enterEvent.which = 13; + branchNameEl().dispatchEvent(enterEvent); + + expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); + }); + }); +}); diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js new file mode 100644 index 00000000000..4fb79663c51 --- /dev/null +++ b/spec/javascripts/blob/target_branch_dropdown_spec.js @@ -0,0 +1,119 @@ +require('~/gl_dropdown'); +require('~/lib/utils/type_utility'); +require('~/blob/create_branch_dropdown'); +require('~/blob/target_branch_dropdown'); + +describe('TargetBranchDropdown', () => { + const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; + let dropdown; + + function createDropdown() { + const projectBranches = getJSONFixture('project_branches.json'); + const dropdownEl = document.querySelector('.js-project-branches-dropdown'); + dropdown = new gl.TargetBranchDropDown(dropdownEl); + dropdown.cachedRefs = projectBranches; + dropdown.refreshData(); + return dropdown; + } + + function submitBtn() { + return document.querySelector('button[type="submit"]'); + } + + function searchField() { + return document.querySelector('.dropdown-page-one .dropdown-input-field'); + } + + function element() { + return document.querySelectorAll('div.dropdown-content li a'); + } + + function elementAtIndex(index) { + return element()[index]; + } + + function clickElementAtIndex(index) { + elementAtIndex(index).click(); + } + + preloadFixtures(fixtureTemplate); + + beforeEach(() => { + loadFixtures(fixtureTemplate); + createDropdown(); + }); + + it('disable submit when branch is not selected', () => { + document.querySelector('input[name="target_branch"]').value = null; + clickElementAtIndex(1); + + expect(submitBtn().getAttribute('disabled')).toEqual(''); + }); + + it('enable submit when a branch is selected', () => { + clickElementAtIndex(1); + + expect(submitBtn().getAttribute('disabled')).toBe(null); + }); + + it('triggers change.branch event on a branch click', () => { + spyOnEvent(dropdown.$dropdown, 'change.branch'); + clickElementAtIndex(0); + + expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown); + }); + + describe('#dropdownData', () => { + it('cache the refs', () => { + const refs = dropdown.cachedRefs; + dropdown.cachedRefs = null; + + dropdown.dropdownData(refs); + + expect(dropdown.cachedRefs).toEqual(refs); + }); + + it('returns the Branches with the newBranch and defaultBranch', () => { + const refs = dropdown.cachedRefs; + dropdown.branchInput.value = 'master'; + dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' }; + + const branches = dropdown.dropdownData(refs).Branches; + + expect(branches.length).toEqual(4); + expect(branches[0]).toEqual(dropdown.newBranch); + expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' }); + expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' }); + expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' }); + }); + }); + + describe('#setNewBranch', () => { + it('adds the new branch and select it', () => { + const branchName = 'new_branch'; + + dropdown.setNewBranch(branchName); + + expect(elementAtIndex(0)).toHaveClass('is-active'); + expect(elementAtIndex(0)).toContainHtml(branchName); + }); + + it("doesn't add a new branch if already exists in the list", () => { + const branchName = elementAtIndex(0).text; + const initialLength = element().length; + + dropdown.setNewBranch(branchName); + + expect(element().length).toEqual(initialLength); + }); + + it('clears the search filter', () => { + const branchName = elementAtIndex(0).text; + searchField().value = 'searching'; + + dropdown.setNewBranch(branchName); + + expect(searchField().value).toEqual(''); + }); + }); +}); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 22c9f12951b..4999933c0c1 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -8,7 +8,6 @@ import boardNewIssue from '~/boards/components/board_new_issue'; require('~/boards/models/list'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Issue boards new issue form', () => { let vm; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 49a2ca4a78f..1d1069600fc 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -15,7 +15,6 @@ require('~/boards/models/user'); require('~/boards/services/board_service'); require('~/boards/stores/boards_store'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Store', () => { beforeEach(() => { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js index 096d3272eac..48994b7c523 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/bootstrap_jquery_spec.js @@ -1,9 +1,9 @@ /* eslint-disable space-before-function-paren, no-var */ -require('~/extensions/jquery'); +import '~/commons/bootstrap'; (function() { - describe('jQuery extensions', function() { + describe('Bootstrap jQuery extensions', function() { describe('disable', function() { beforeEach(function() { return setFixtures('<input type="text" />'); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index d50d45d295e..85b73f1d4e2 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,14 +1,16 @@ -const ActionsComponent = require('~/environments/components/environment_actions'); +import Vue from 'vue'; +import actionsComp from '~/environments/components/environment_actions'; describe('Actions Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ActionsComponent; + let actionsMock; + let spy; + let component; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); - }); + ActionsComponent = Vue.extend(actionsComp); - it('should render a dropdown with the provided actions', () => { - const actionsMock = [ + actionsMock = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -19,18 +21,27 @@ describe('Actions Component', () => { }, ]; - const component = new ActionsComponent({ - el: document.querySelector('.test-dom-element'), + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + component = new ActionsComponent({ propsData: { actions: actionsMock, + service: { + postAction: spy, + }, }, - }); + }).$mount(); + }); + it('should render a dropdown with the provided actions', () => { expect( component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); - expect( - component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), - ).toEqual(actionsMock[0].play_path); + }); + + it('should call the service when an action is clicked', () => { + component.$el.querySelector('.dropdown').click(); + component.$el.querySelector('.js-manual-action-link').click(); + + expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path); }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js index 393dbb5aae0..9af218a27ff 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js +++ b/spec/javascripts/environments/environment_external_url_spec.js @@ -1,19 +1,20 @@ -const ExternalUrlComponent = require('~/environments/components/environment_external_url'); +import Vue from 'vue'; +import externalUrlComp from '~/environments/components/environment_external_url'; describe('External URL Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ExternalUrlComponent; + beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + ExternalUrlComponent = Vue.extend(externalUrlComp); }); it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; const component = new ExternalUrlComponent({ - el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, }, - }); + }).$mount(); expect(component.$el.getAttribute('href')).toEqual(externalURL); expect(component.$el.querySelector('fa-external-link')).toBeDefined(); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 7fea80ed799..4d42de4d549 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -1,10 +1,12 @@ -window.timeago = require('timeago.js'); -const EnvironmentItem = require('~/environments/components/environment_item'); +import 'timeago.js'; +import Vue from 'vue'; +import environmentItemComp from '~/environments/components/environment_item'; describe('Environment item', () => { - preloadFixtures('static/environments/table.html.raw'); + let EnvironmentItem; + beforeEach(() => { - loadFixtures('static/environments/table.html.raw'); + EnvironmentItem = Vue.extend(environmentItemComp); }); describe('When item is folder', () => { @@ -21,13 +23,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('Should render folder icon and name', () => { @@ -109,13 +111,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: environment, canCreateDeployment: true, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('should render environment name', () => { diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index 4a596baad09..7cb39d9df03 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -1,47 +1,59 @@ -const RollbackComponent = require('~/environments/components/environment_rollback'); +import Vue from 'vue'; +import rollbackComp from '~/environments/components/environment_rollback'; describe('Rollback Component', () => { - preloadFixtures('static/environments/element.html.raw'); - const retryURL = 'https://gitlab.com/retry'; + let RollbackComponent; + let spy; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + RollbackComponent = Vue.extend(rollbackComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); }); - it('Should link to the provided retryUrl', () => { + it('Should render Re-deploy label when isLastDeployment is true', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: true, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.getAttribute('href')).toEqual(retryURL); + expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); }); - it('Should render Re-deploy label when isLastDeployment is true', () => { + it('Should render Rollback label when isLastDeployment is false', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, - isLastDeployment: true, + isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); + expect(component.$el.querySelector('span').textContent).toContain('Rollback'); }); - it('Should render Rollback label when isLastDeployment is false', () => { + it('should call the service when the button is clicked', () => { const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Rollback'); + component.$el.click(); + + expect(spy).toHaveBeenCalledWith(retryURL); }); }); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index edd0cad32d0..9601575577e 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsComponent = require('~/environments/components/environment'); -const { environment } = require('./mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsComponent from '~/environments/components/environment'; +import { environment } from './mock_data'; describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 5ca65b1debc..8f79b88f3df 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -1,28 +1,34 @@ -const StopComponent = require('~/environments/components/environment_stop'); +import Vue from 'vue'; +import stopComp from '~/environments/components/environment_stop'; describe('Stop Component', () => { - preloadFixtures('static/environments/element.html.raw'); - - let stopURL; + let StopComponent; let component; + let spy; + const stopURL = '/stop'; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + StopComponent = Vue.extend(stopComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + spyOn(window, 'confirm').and.returnValue(true); - stopURL = '/stop'; component = new StopComponent({ - el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, + service: { + postAction: spy, + }, }, - }); + }).$mount(); }); - it('should link to the provided URL', () => { - expect(component.$el.getAttribute('href')).toEqual(stopURL); + it('should render a button to stop the environment', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + expect(component.$el.getAttribute('title')).toEqual('Stop Environment'); }); - it('should have a data-confirm attribute', () => { - expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?'); + it('should call the service when an action is clicked', () => { + component.$el.click(); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index be4330b5012..3df967848a7 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,4 +1,5 @@ -const EnvironmentTable = require('~/environments/components/environments_table'); +import Vue from 'vue'; +import environmentTableComp from '~/environments/components/environments_table'; describe('Environment item', () => { preloadFixtures('static/environments/element.html.raw'); @@ -16,14 +17,17 @@ describe('Environment item', () => { }, }; + const EnvironmentTable = Vue.extend(environmentTableComp); + const component = new EnvironmentTable({ el: document.querySelector('.test-dom-element'), propsData: { environments: [{ mockItem }], canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); expect(component.$el.tagName).toEqual('TABLE'); }); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js new file mode 100644 index 00000000000..b07aa4e1745 --- /dev/null +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import terminalComp from '~/environments/components/environment_terminal_button'; + +describe('Stop Component', () => { + let TerminalComponent; + let component; + const terminalPath = '/path'; + + beforeEach(() => { + TerminalComponent = Vue.extend(terminalComp); + + component = new TerminalComponent({ + propsData: { + terminalPath, + }, + }).$mount(); + }); + + it('should render a link to open a web terminal with the provided path', () => { + expect(component.$el.tagName).toEqual('A'); + expect(component.$el.getAttribute('title')).toEqual('Open web terminal'); + expect(component.$el.getAttribute('href')).toEqual(terminalPath); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js index 77e182b3830..115d84b50f5 100644 --- a/spec/javascripts/environments/environments_store_spec.js +++ b/spec/javascripts/environments/environments_store_spec.js @@ -1,5 +1,5 @@ -const Store = require('~/environments/stores/environments_store'); -const { environmentsList, serverData } = require('./mock_data'); +import Store from '~/environments/stores/environments_store'; +import { environmentsList, serverData } from './mock_data'; (() => { describe('Store', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index d1335b5b304..43a217a67f5 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); -const { environmentsList } = require('../mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view'; +import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { preloadFixtures('static/environments/environments_folder_view.html.raw'); diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js index 5c395c6b2d8..30861481cc5 100644 --- a/spec/javascripts/environments/mock_data.js +++ b/spec/javascripts/environments/mock_data.js @@ -1,4 +1,4 @@ -const environmentsList = [ +export const environmentsList = [ { name: 'DEV', size: 1, @@ -30,7 +30,7 @@ const environmentsList = [ }, ]; -const serverData = [ +export const serverData = [ { name: 'DEV', size: 1, @@ -67,7 +67,7 @@ const serverData = [ }, ]; -const environment = { +export const environment = { name: 'DEV', size: 1, latest: { @@ -84,9 +84,3 @@ const environment = { updated_at: '2017-01-31T10:53:46.894Z', }, }; - -module.exports = { - environmentsList, - environment, - serverData, -}; diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js index 60f6b9b78e3..4b871fe967d 100644 --- a/spec/javascripts/extensions/array_spec.js +++ b/spec/javascripts/extensions/array_spec.js @@ -18,28 +18,5 @@ require('~/extensions/array'); return expect(arr.last()).toBe(5); }); }); - - describe('find', function () { - beforeEach(() => { - this.arr = [0, 1, 2, 3, 4, 5]; - }); - - it('returns the item that first passes the predicate function', () => { - expect(this.arr.find(item => item === 2)).toBe(2); - }); - - it('returns undefined if no items pass the predicate function', () => { - expect(this.arr.find(item => item === 6)).not.toBeDefined(); - }); - - it('error when called on undefined or null', () => { - expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow(); - expect(Array.prototype.find.bind(null, item => item === 1)).toThrow(); - }); - - it('error when predicate is not a function', () => { - expect(Array.prototype.find.bind(this.arr, 1)).toThrow(); - }); - }); }); }).call(window); diff --git a/spec/javascripts/extensions/element_spec.js b/spec/javascripts/extensions/element_spec.js deleted file mode 100644 index 2d8a128ed33..00000000000 --- a/spec/javascripts/extensions/element_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -require('~/extensions/element'); - -(() => { - describe('Element extensions', function () { - beforeEach(() => { - this.element = document.createElement('ul'); - }); - - describe('matches', () => { - it('returns true if element matches the selector', () => { - expect(this.element.matches('ul')).toBeTruthy(); - }); - - it("returns false if element doesn't match the selector", () => { - expect(this.element.matches('.not-an-element')).toBeFalsy(); - }); - }); - - describe('closest', () => { - beforeEach(() => { - this.childElement = document.createElement('li'); - this.element.appendChild(this.childElement); - }); - - it('returns the closest parent that matches the selector', () => { - expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); - }); - - it('returns itself if it matches the selector', () => { - expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); - }); - - it('returns undefined if nothing matches the selector', () => { - expect(this.childElement.closest('.no-an-element')).toBeFalsy(); - }); - }); - }); -})(); diff --git a/spec/javascripts/extensions/object_spec.js b/spec/javascripts/extensions/object_spec.js deleted file mode 100644 index 2467ed78459..00000000000 --- a/spec/javascripts/extensions/object_spec.js +++ /dev/null @@ -1,25 +0,0 @@ -require('~/extensions/object'); - -describe('Object extensions', () => { - describe('assign', () => { - it('merges source object into target object', () => { - const targetObj = {}; - const sourceObj = { - foo: 'bar', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('bar'); - }); - - it('merges object with the same properties', () => { - const targetObj = { - foo: 'bar', - }; - const sourceObj = { - foo: 'baz', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('baz'); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 5c65903701b..e6538020896 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -126,7 +126,11 @@ require('~/filtered_search/filtered_search_dropdown_manager'); beforeEach(() => { setFixtures(` - <input type="text" id="test" /> + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search" type="text" id="test" /> + </li> + </ul> `); input = document.getElementById('test'); @@ -142,7 +146,7 @@ require('~/filtered_search/filtered_search_dropdown_manager'); input.value = 'o'; updatedItem = gl.DropdownUtils.filterHint(input, { hint: 'label', - }, 'o'); + }); expect(updatedItem.droplab_hidden).toBe(true); }); @@ -150,6 +154,29 @@ require('~/filtered_search/filtered_search_dropdown_manager'); const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); + + it('should allow multiple if item.type is array', () => { + input.value = 'label:~first la'; + const updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', + type: 'array', + }); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should prevent multiple if item.type is not array', () => { + input.value = 'milestone:~first mile'; + let updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', + }); + expect(updatedItem.droplab_hidden).toBe(true); + + updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', + type: 'string', + }); + expect(updatedItem.droplab_hidden).toBe(true); + }); }); describe('setDataValueIfSelected', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 81c1d81d181..ae9c263d1d7 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -41,7 +41,6 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper </div> `); - spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); @@ -54,6 +53,10 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper manager = new gl.FilteredSearchManager(); }); + afterEach(() => { + manager.cleanup(); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=✓&state=opened'; diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json new file mode 100644 index 00000000000..a96a4c0c095 --- /dev/null +++ b/spec/javascripts/fixtures/project_branches.json @@ -0,0 +1,5 @@ +[ + "master", + "development", + "staging" +] diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml new file mode 100644 index 00000000000..821fb7940a0 --- /dev/null +++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml @@ -0,0 +1,28 @@ +%form.js-edit-blob-form + %input{type: 'hidden', name: 'target_branch', value: 'master'} + %div + .dropdown + %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}} + .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging + .dropdown-page-one + .dropdown-title 'Select branch' + .dropdown-input + %input.dropdown-input-field{type: 'search', value: ''} + %i.fa.fa-search.dropdown-input-search + %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'} + .dropdown-content + .dropdown-footer + %ul.dropdown-footer-list + %li + %a.create-new-branch.dropdown-toggle-page{href: "#"} + Create new branch + .dropdown-page-two.dropdown-new-branch + %button.dropdown-title-button.dropdown-menu-back{type: 'button'} + .dropdown_title 'Create new branch' + .dropdown_content + %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" } + %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" } + Create + %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" } + Cancel + %button{type: 'submit'} diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index 7ab0b37f2ec..9b44b25980c 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -1,6 +1,3 @@ -import '~/extensions/string'; -import '~/extensions/array'; - import { glEmojiTag } from '~/behaviors/gl_emoji'; import { isEmojiUnicodeSupported, diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index a0b2ebc221b..a1fd2d38968 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -7,16 +7,12 @@ require('~/line_highlighter'); describe('LineHighlighter', function() { var clickLine; preloadFixtures('static/line_highlighter.html.raw'); - clickLine = function(number, eventData) { - var e; - if (eventData == null) { - eventData = {}; - } + clickLine = function(number, eventData = {}) { if ($.isEmptyObject(eventData)) { - return $("#L" + number).mousedown().click(); + return $("#L" + number).click(); } else { - e = $.Event('mousedown', eventData); - return $("#L" + number).trigger(e).click(); + const e = $.Event('click', eventData); + return $("#L" + number).trigger(e); } }; beforeEach(function() { @@ -63,12 +59,6 @@ require('~/line_highlighter'); }); }); describe('#clickHandler', function() { - it('discards the mousedown event', function() { - var spy; - spy = spyOnEvent('a[data-line-number]', 'mousedown'); - clickLine(13); - return expect(spy).toHaveBeenPrevented(); - }); it('handles clicking on a child icon element', function() { var spy; spy = spyOn(this["class"], 'setHash').and.callThrough(); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 823b4bab7fc..a3c1c5e1b7c 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,11 +1,8 @@ import 'jquery'; -import es6Promise from 'es6-promise'; import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; -es6Promise.polyfill(); - describe('PrometheusGraph', () => { const fixtureName = 'static/environments/metrics.html.raw'; const prometheusGraphContainer = '.prometheus-graph'; diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js new file mode 100644 index 00000000000..ecaaf1907ea --- /dev/null +++ b/spec/javascripts/polyfills/element_spec.js @@ -0,0 +1,36 @@ +import '~/commons/polyfills/element'; + +describe('Element polyfills', function () { + beforeEach(() => { + this.element = document.createElement('ul'); + }); + + describe('matches', () => { + it('returns true if element matches the selector', () => { + expect(this.element.matches('ul')).toBeTruthy(); + }); + + it("returns false if element doesn't match the selector", () => { + expect(this.element.matches('.not-an-element')).toBeFalsy(); + }); + }); + + describe('closest', () => { + beforeEach(() => { + this.childElement = document.createElement('li'); + this.element.appendChild(this.childElement); + }); + + it('returns the closest parent that matches the selector', () => { + expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); + }); + + it('returns itself if it matches the selector', () => { + expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); + }); + + it('returns undefined if nothing matches the selector', () => { + expect(this.childElement.closest('.no-an-element')).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 69d9587771f..3a1d4e2440f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -26,7 +26,7 @@ require('~/project'); var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 }); + expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); d = $.Deferred(); d.resolve(this.projects_data); return d.promise(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 4ac7e911740..285b7940174 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ /* global Sidebar */ -require('~/right_sidebar'); -require('~/extensions/jquery.js'); +import '~/commons/bootstrap'; +import '~/right_sidebar'; (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index ffff643e371..9e19dabd0e3 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -31,13 +31,9 @@ require('~/shortcuts_issuable'); this.shortcut.replyWithSelectedText(); expect($(this.selector).val()).toBe(''); }); - it('triggers `input`', function() { - var focused = false; - $(this.selector).on('focus', function() { - focused = true; - }); + it('triggers `focus`', function() { this.shortcut.replyWithSelectedText(); - expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with any selection', function() { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index fae462561e9..c12b44cea89 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -48,10 +48,10 @@ describe('Uncovered files', function () { './network/branch_graph.js', ]; - const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/); + const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.js$/); sourceFiles.keys().forEach(function (path) { // ignore if there is a matching spec file - if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) { + if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { return; } diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 69e3c52b35a..63fb1bb25c4 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do context "when no language is specified" do it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>') end end context "when a valid language is specified" do it "highlights as that language" do result = filter('<pre><code class="ruby">def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>') end end context "when an invalid language is specified" do it "highlights as plaintext" do result = filter('<pre><code class="gnuplot">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>') end end diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 730ca1f7c0a..90628917943 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -45,10 +45,10 @@ describe ExpandVariables do { key: 'variable', value: 'value' }, { key: 'variable2', value: 'result' } ] }, - { value: 'review/$CI_BUILD_REF_NAME', + { value: 'review/$CI_COMMIT_REF_NAME', result: 'review/feature/add-review-apps', variables: [ - { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' } + { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } ] }, ] diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index cadfbadca10..e22f88b7a32 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -12,8 +12,16 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ref: 'refs/heads/master' } end - - subject { described_class.new(changes, project: project, user_access: user_access).exec } + let(:protocol) { 'ssh' } + + subject do + described_class.new( + changes, + project: project, + user_access: user_access, + protocol: protocol + ).exec + end before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) } diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 2adbed2154f..c330e609337 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -151,8 +151,8 @@ describe Gitlab::Ci::Config::Entry::Environment do context 'when variables are used for environment' do let(:config) do - { name: 'review/$CI_BUILD_REF_NAME', - url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' } + { name: 'review/$CI_COMMIT_REF_NAME', + url: 'https://$CI_COMMIT_REF_NAME.review.gitlab.com' } end describe '#valid?' do diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb index 16eb3766356..2570f95dd21 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/conflict/parser_spec.rb @@ -120,43 +120,61 @@ CONFLICT end context 'when the file contents include conflict delimiters' do - it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do - expect { parse_text('=======') }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) - - expect { parse_text('>>>>>>> README.md') }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) - - expect { parse_text('>>>>>>> some-other-path.md') }. - not_to raise_error + context 'when there is a non-start delimiter first' do + it 'raises UnexpectedDelimiter when there is a middle delimiter first' do + expect { parse_text('=======') }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end + + it 'raises UnexpectedDelimiter when there is an end delimiter first' do + expect { parse_text('>>>>>>> README.md') }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end + + it 'does not raise when there is an end delimiter for a different path first' do + expect { parse_text('>>>>>>> some-other-path.md') }. + not_to raise_error + end end - it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do - start_text = "<<<<<<< README.md\n" - end_text = "\n=======\n>>>>>>> README.md" + context 'when a start delimiter is followed by a non-middle delimiter' do + let(:start_text) { "<<<<<<< README.md\n" } + let(:end_text) { "\n=======\n>>>>>>> README.md" } - expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do + expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end - expect { parse_text(start_text + start_text + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do + expect { parse_text(start_text + start_text + end_text) }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end - expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }. - not_to raise_error + it 'does not raise when it is followed by a start delimiter for a different path' do + expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }. + not_to raise_error + end end - it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do - start_text = "<<<<<<< README.md\n=======\n" - end_text = "\n>>>>>>> README.md" + context 'when a middle delimiter is followed by a non-end delimiter' do + let(:start_text) { "<<<<<<< README.md\n=======\n" } + let(:end_text) { "\n>>>>>>> README.md" } - expect { parse_text(start_text + '=======' + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do + expect { parse_text(start_text + '=======' + end_text) }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end - expect { parse_text(start_text + start_text + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do + expect { parse_text(start_text + start_text + end_text) }. + to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + end - expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }. - not_to raise_error + it 'does not raise when it is followed by a start delimiter for another path' do + expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }. + not_to raise_error + end end it 'raises MissingEndDelimiter when there is no end delimiter at the end' do @@ -184,9 +202,20 @@ CONFLICT to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end - it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do - expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }. - to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + # All text from Rugged has an encoding of ASCII_8BIT, so force that in + # these strings. + context 'when the file contains UTF-8 characters' do + it 'does not raise' do + expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }. + not_to raise_error + end + end + + context 'when the file contains non-UTF-8 characters' do + it 'raises UnsupportedEncoding' do + expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }. + to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + end end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 0e9309d278e..c6bd4e81f4f 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'highlights and marks unchanged lines' do - code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n} + code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n} expect(subject[2].text).to eq(code) end it 'highlights and marks removed lines' do - code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} + code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} expect(subject[4].text).to eq(code) end it 'highlights and marks added lines' do - code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} + code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} expect(subject[5].text).to eq(code) end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index b983d73f8be..e76128ecd87 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -91,6 +91,54 @@ eos end end + describe '\ No newline at end of file' do + it "parses nonewline in one file correctly" do + first_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + lorem + -ipsum + \\ No newline at end of file + END + lines = parser.parse(first_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('new') + expect(lines[0].text).to eq('+ipsum') + expect(lines[2].type).to eq('old') + expect(lines[3].type).to eq('old-nonewline') + expect(lines[1].old_pos).to eq(1) + expect(lines[1].new_pos).to eq(2) + end + + it "parses nonewline in two files correctly" do + both_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + -lorem + -ipsum + \\ No newline at end of file + +ipsum + +lorem + \\ No newline at end of file + END + lines = parser.parse(both_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('old') + expect(lines[1].type).to eq('old') + expect(lines[2].type).to eq('old-nonewline') + expect(lines[5].type).to eq('new-nonewline') + expect(lines[3].text).to eq('+ipsum') + expect(lines[3].old_pos).to eq(3) + expect(lines[3].new_pos).to eq(1) + expect(lines[4].text).to eq('+lorem') + expect(lines[4].old_pos).to eq(3) + expect(lines[4].new_pos).to eq(2) + end + end + context 'when lines is empty' do it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index bc139d5ef28..9ed43da1116 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -90,6 +90,12 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(prefix).to eq("#{project_name}-test-branch-SHA") end + + it 'returns correct string for a ref containing dots' do + prefix = repository.archive_prefix('test.branch', 'SHA') + + expect(prefix).to eq("#{project_name}-test.branch-SHA") + end end describe '#archive' do @@ -507,7 +513,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_add" do before(:all) do @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) - @repo.remote_add("new_remote", SeedHelper::GITLAB_URL) + @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) end it "should add the remote" do diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 3f080de99dd..8b867fbe322 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) end - let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:label1) do double( name: 'Bug', @@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end - let!(:user) { create(:user, email: octocat.email) } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", - labels: [double(name: 'Label #2')] - ) - end - let(:release1) do double( tag_name: 'v1.0.0', @@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + subject { described_class.new(project) } + it 'returns true' do - expect(described_class.new(project).execute).to eq true + expect(subject.execute).to eq true end it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error + expect { subject.execute }.not_to raise_error end it 'stores error messages' do @@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do end end + shared_examples 'Gitlab::GithubImport unit-testing' do + describe '#clean_up_restored_branches' do + subject { described_class.new(project) } + + before do + allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false } + allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false } + end + + context 'when pull request stills open' do + let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) } + + it 'does not remove branches' do + expect(subject).not_to receive(:remove_branch) + subject.send(:clean_up_restored_branches, gh_pull_request) + end + end + + context 'when pull request is closed' do + let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) } + + it 'does remove branches' do + expect(subject).to receive(:remove_branch).at_least(2).times + subject.send(:clean_up_restored_branches, gh_pull_request) + end + end + end + end + let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:credentials) { { user: 'joe' } } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + let(:closed_pull_request) do + double( + number: 1347, + milestone: nil, + state: 'closed', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: updated_at, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + context 'when importing a GitHub project' do let(:api_root) { 'https://api.github.com' } let(:repo_root) { 'https://github.com' } + subject { described_class.new(project) } it_behaves_like 'Gitlab::GithubImport::Importer#execute' it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + it_behaves_like 'Gitlab::GithubImport unit-testing' describe '#client' do it 'instantiates a Client' do @@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do {} ) - described_class.new(project).client + subject.client end end end @@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do context 'when importing a Gitea project' do let(:api_root) { 'https://try.gitea.io/api/v1' } let(:repo_root) { 'https://try.gitea.io' } + subject { described_class.new(project) } + before do project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") end @@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do let(:expected_not_called) { [:import_releases] } end it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + it_behaves_like 'Gitlab::GithubImport unit-testing' describe '#client' do it 'instantiates a Client' do @@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do { host: "#{repo_root}:443/foo", api_version: 'v1' } ) - described_class.new(project).client + subject.client end end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 951cbea7857..44423917944 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -306,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' end end + + describe '#opened?' do + let(:raw_data) { double(base_data.merge(state: 'open')) } + + it 'returns true when state is "open"' do + expect(pull_request.opened?).to be_truthy + end + end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e177d883158..e49799ad105 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do end it 'highlights all the lines properly' do - expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) - expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) - expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) + expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) + expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) + expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) end describe 'with CRLF' do @@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do end it 'strips extra LFs' do - expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>") + expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>") end end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 917c5c46db1..8b77c925705 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -3,8 +3,16 @@ require 'spec_helper' describe Gitlab::Redis do include StubENV - before(:each) { clear_raw_config } - after(:each) { clear_raw_config } + let(:config) { 'config/resque.yml' } + + before(:each) do + stub_env('GITLAB_REDIS_CONFIG_FILE', Rails.root.join(config).to_s) + clear_raw_config + end + + after(:each) do + clear_raw_config + end describe '.params' do subject { described_class.params } @@ -18,22 +26,22 @@ describe Gitlab::Redis do end context 'when url contains unix socket reference' do - let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s } - let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s } + let(:config_old) { 'spec/fixtures/config/redis_old_format_socket.yml' } + let(:config_new) { 'spec/fixtures/config/redis_new_format_socket.yml' } context 'with old format' do - it 'returns path key instead' do - stub_const("#{described_class}::CONFIG_FILE", config_old) + let(:config) { config_old } + it 'returns path key instead' do is_expected.to include(path: '/path/to/old/redis.sock') is_expected.not_to have_key(:url) end end context 'with new format' do - it 'returns path key instead' do - stub_const("#{described_class}::CONFIG_FILE", config_new) + let(:config) { config_new } + it 'returns path key instead' do is_expected.to include(path: '/path/to/redis.sock') is_expected.not_to have_key(:url) end @@ -41,22 +49,22 @@ describe Gitlab::Redis do end context 'when url is host based' do - let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } - let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + let(:config_old) { 'spec/fixtures/config/redis_old_format_host.yml' } + let(:config_new) { 'spec/fixtures/config/redis_new_format_host.yml' } context 'with old format' do - it 'returns hash with host, port, db, and password' do - stub_const("#{described_class}::CONFIG_FILE", config_old) + let(:config) { config_old } + it 'returns hash with host, port, db, and password' do is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) is_expected.not_to have_key(:url) end end context 'with new format' do - it 'returns hash with host, port, db, and password' do - stub_const("#{described_class}::CONFIG_FILE", config_new) + let(:config) { config_new } + it 'returns hash with host, port, db, and password' do is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99) is_expected.not_to have_key(:url) end @@ -74,15 +82,13 @@ describe Gitlab::Redis do end context 'when yml file with env variable' do - let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') } + let(:config) { 'spec/fixtures/config/redis_config_with_env.yml' } before do stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379') end it 'reads redis url from env variable' do - stub_const("#{described_class}::CONFIG_FILE", redis_config) - expect(described_class.url).to eq 'redis://redishost:6379' end end @@ -90,14 +96,13 @@ describe Gitlab::Redis do describe '._raw_config' do subject { described_class._raw_config } + let(:config) { '/var/empty/doesnotexist' } it 'should be frozen' do expect(subject).to be_frozen end it 'returns false when the file does not exist' do - stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist') - expect(subject).to eq(false) end end @@ -134,22 +139,18 @@ describe Gitlab::Redis do subject { described_class.new(Rails.env).sentinels } context 'when sentinels are defined' do - let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' } it 'returns an array of hashes with host and port keys' do - stub_const("#{described_class}::CONFIG_FILE", config) - is_expected.to include(host: 'localhost', port: 26380) is_expected.to include(host: 'slave2', port: 26381) end end context 'when sentinels are not defined' do - let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' } it 'returns nil' do - stub_const("#{described_class}::CONFIG_FILE", config) - is_expected.to be_nil end end @@ -159,21 +160,17 @@ describe Gitlab::Redis do subject { described_class.new(Rails.env).sentinels? } context 'when sentinels are defined' do - let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' } it 'returns true' do - stub_const("#{described_class}::CONFIG_FILE", config) - is_expected.to be_truthy end end context 'when sentinels are not defined' do - let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' } it 'returns false' do - stub_const("#{described_class}::CONFIG_FILE", config) - is_expected.to be_falsey end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index b692142713f..e822d7eb348 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -8,6 +8,15 @@ describe Notify do include_context 'gitlab email notification' + def have_referable_subject(referable, reply: false) + prefix = referable.project.name if referable.project + prefix = "Re: #{prefix}" if reply + + suffix = "#{referable.title} (#{referable.to_reference})" + + have_subject [prefix, suffix].compact.join(' | ') + end + context 'for a project' do describe 'items that are assignable, the email' do let(:current_user) { create(:user, email: "current@email.com") } @@ -41,11 +50,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue) end it 'contains a link to the new issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end context 'when enabled email_author_in_body' do @@ -55,7 +64,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text issue.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -66,7 +75,7 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it 'contains the description' do - is_expected.to have_body_text /#{issue_with_description.description}/ + is_expected.to have_body_text issue_with_description.description end end @@ -87,19 +96,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -121,15 +130,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -150,19 +159,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) end end @@ -181,7 +190,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains link to new issue' do @@ -191,7 +200,7 @@ describe Notify do end it 'contains a link to the original issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end end @@ -212,19 +221,19 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request) end it 'contains a link to the new merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end it 'contains the source branch for the merge request' do - is_expected.to have_body_text /#{merge_request.source_branch}/ + is_expected.to have_body_text merge_request.source_branch end it 'contains the target branch for the merge request' do - is_expected.to have_body_text /#{merge_request.target_branch}/ + is_expected.to have_body_text merge_request.target_branch end context 'when enabled email_author_in_body' do @@ -234,7 +243,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text merge_request.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -246,7 +255,7 @@ describe Notify do it_behaves_like "an unsubscribeable thread" it 'contains the description' do - is_expected.to have_body_text /#{merge_request_with_description.description}/ + is_expected.to have_body_text merge_request_with_description.description end end @@ -267,19 +276,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -301,15 +310,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -330,19 +339,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -363,15 +372,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /merged/i + is_expected.to have_body_text 'merged' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end end @@ -387,15 +396,15 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do - is_expected.to have_subject /Project was moved/ + is_expected.to have_subject "#{project.name} | Project was moved" end it 'contains name of project' do - is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text project.name_with_namespace end it 'contains new user role' do - is_expected.to have_body_text /#{project.ssh_url_to_repo}/ + is_expected.to have_body_text project.ssh_url_to_repo end end @@ -424,9 +433,9 @@ describe Notify do expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end @@ -451,9 +460,9 @@ describe Notify do expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end end @@ -473,8 +482,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url end end @@ -490,9 +499,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access end end @@ -521,10 +530,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ - is_expected.to have_body_text /#{project_member.invite_token}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access + is_expected.to have_body_text project_member.invite_token end end @@ -546,10 +555,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -570,9 +579,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email end end @@ -598,11 +607,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -612,7 +621,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -632,7 +641,7 @@ describe Notify do it_behaves_like 'a user cannot unsubscribe through footer link' it 'has the correct subject' do - is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/ + is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})" end it 'contains a link to the commit' do @@ -655,11 +664,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains a link to the merge request note' do - is_expected.to have_body_text /#{note_on_merge_request_path}/ + is_expected.to have_body_text note_on_merge_request_path end end @@ -678,11 +687,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains a link to the issue note' do - is_expected.to have_body_text /#{note_on_issue_path}/ + is_expected.to have_body_text note_on_issue_path end end end @@ -698,11 +707,11 @@ describe Notify do let(:note) { create(model, project: project, author: note_author) } it "includes diffs with character-level highlighting" do - is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/ + is_expected.to have_body_text '<span class="p">}</span></span>' end it 'contains a link to the diff file' do - is_expected.to have_body_text /#{note.diff_file.file_path}/ + is_expected.to have_body_text note.diff_file.file_path end it_behaves_like 'it should have Gmail Actions links' @@ -718,11 +727,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -732,7 +741,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -777,9 +786,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Request to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group_group_members_url(group)}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group_group_members_url(group) + is_expected.to have_body_text group_member.human_access end end @@ -798,8 +807,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was denied" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url end end @@ -816,9 +825,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was granted" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access end end @@ -847,10 +856,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ - is_expected.to have_body_text /#{group_member.invite_token}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access + is_expected.to have_body_text group_member.invite_token end end @@ -872,10 +881,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -896,9 +905,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email end end end @@ -925,11 +934,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /^Confirmation instructions/ + is_expected.to have_subject 'Confirmation instructions | A Nice Suffix' end it 'includes a link to the site' do - is_expected.to have_body_text /#{example_site_path}/ + is_expected.to have_body_text example_site_path end end @@ -952,11 +961,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master" end it 'contains a link to the branch' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -979,11 +988,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0" end it 'contains a link to the tag' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -1005,7 +1014,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted branch master" end end @@ -1027,7 +1036,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted tag v1.0" end end @@ -1055,23 +1064,23 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/ + is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end it 'does not contain the misleading footer' do - is_expected.not_to have_body_text /you are a member of/ + is_expected.not_to have_body_text 'you are a member of' end context "when set to send from committer email if domain matches" do @@ -1157,19 +1166,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{commits.first.title}/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/ + is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end end diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb new file mode 100644 index 00000000000..36e82729c23 --- /dev/null +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserved_project_names.rb') + +# This migration uses multiple threads, and thus different transactions. This +# means data created in this spec may not be visible to some threads. To work +# around this we use the TRUNCATE cleaning strategy. +describe RenameMoreReservedProjectNames, truncate: true do + let(:migration) { described_class.new } + let!(:project) { create(:empty_project) } + + before do + project.path = 'artifacts' + project.save!(validate: false) + end + + describe '#up' do + context 'when project repository exists' do + before { project.create_repository } + + context 'when no exception is raised' do + it 'renames project with reserved names' do + migration.up + + expect(project.reload.path).to eq('artifacts0') + end + end + + context 'when exception is raised during rename' do + before do + allow(project).to receive(:rename_repo).and_raise(StandardError) + end + + it 'captures exception from project rename' do + expect { migration.up }.not_to raise_error + end + end + end + + context 'when project repository does not exist' do + it 'does not raise error' do + expect { migration.up }.not_to raise_error + end + end + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 30f8fdf91b2..92d70cfc64c 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -1,6 +1,12 @@ require 'spec_helper' describe Ability, lib: true do + context 'using a nil subject' do + it 'is always empty' do + expect(Ability.allowed(nil, nil).to_set).to be_empty + end + end + describe '.can_edit_note?' do let(:project) { create(:empty_project) } let(:note) { create(:note_on_issue, project: project) } diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 03d02b4d382..94c25a454aa 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -70,6 +70,8 @@ describe Blob do end describe '#to_partial_path' do + let(:project) { double(lfs_enabled?: true) } + def stubbed_blob(overrides = {}) overrides.reverse_merge!( image?: false, @@ -84,34 +86,35 @@ describe Blob do end end - it 'handles LFS pointers' do - blob = stubbed_blob(lfs_pointer?: true) + it 'handles LFS pointers with LFS enabled' do + blob = stubbed_blob(lfs_pointer?: true, text?: true) + expect(blob.to_partial_path(project)).to eq 'download' + end - expect(blob.to_partial_path).to eq 'download' + it 'handles LFS pointers with LFS disabled' do + blob = stubbed_blob(lfs_pointer?: true, text?: true) + project = double(lfs_enabled?: false) + expect(blob.to_partial_path(project)).to eq 'text' end it 'handles SVGs' do blob = stubbed_blob(text?: true, svg?: true) - - expect(blob.to_partial_path).to eq 'image' + expect(blob.to_partial_path(project)).to eq 'image' end it 'handles images' do blob = stubbed_blob(image?: true) - - expect(blob.to_partial_path).to eq 'image' + expect(blob.to_partial_path(project)).to eq 'image' end it 'handles text' do blob = stubbed_blob(text?: true) - - expect(blob.to_partial_path).to eq 'text' + expect(blob.to_partial_path(project)).to eq 'text' end it 'defaults to download' do blob = stubbed_blob - - expect(blob.to_partial_path).to eq 'download' + expect(blob.to_partial_path(project)).to eq 'download' end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 9962c987110..4a664e4fae2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1018,6 +1018,19 @@ describe Ci::Pipeline, models: true do end end + describe '#update_status' do + let(:pipeline) { create(:ci_pipeline, sha: '123456') } + + it 'updates the cached status' do + fake_status = double + # after updating the status, the status is set to `skipped` for this pipeline's builds + expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status) + expect(fake_status).to receive(:store_in_cache_if_needed) + + pipeline.update_status + end + end + describe 'notifications when pipeline success or failed' do let(:project) { create(:project, :repository) } diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/models/ci/pipeline_status_spec.rb new file mode 100644 index 00000000000..bc5b71666c2 --- /dev/null +++ b/spec/models/ci/pipeline_status_spec.rb @@ -0,0 +1,173 @@ +require 'spec_helper' + +describe Ci::PipelineStatus do + let(:project) { create(:project) } + let(:pipeline_status) { described_class.new(project) } + + describe '.load_for_project' do + it "loads the status" do + expect_any_instance_of(described_class).to receive(:load_status) + + described_class.load_for_project(project) + end + end + + describe '#has_status?' do + it "is false when the status wasn't loaded yet" do + expect(pipeline_status.has_status?).to be_falsy + end + + it 'is true when all status information was loaded' do + fake_commit = double + allow(fake_commit).to receive(:status).and_return('failed') + allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6') + allow(pipeline_status).to receive(:commit).and_return(fake_commit) + allow(pipeline_status).to receive(:has_cache?).and_return(false) + + pipeline_status.load_status + + expect(pipeline_status.has_status?).to be_truthy + end + end + + describe '#load_status' do + it 'loads the status from the cache when there is one' do + expect(pipeline_status).to receive(:has_cache?).and_return(true) + expect(pipeline_status).to receive(:load_from_cache) + + pipeline_status.load_status + end + + it 'loads the status from the project commit when there is no cache' do + allow(pipeline_status).to receive(:has_cache?).and_return(false) + + expect(pipeline_status).to receive(:load_from_commit) + + pipeline_status.load_status + end + + it 'stores the status in the cache when it loading it from the project' do + allow(pipeline_status).to receive(:has_cache?).and_return(false) + allow(pipeline_status).to receive(:load_from_commit) + + expect(pipeline_status).to receive(:store_in_cache) + + pipeline_status.load_status + end + + it 'sets the state to loaded' do + pipeline_status.load_status + + expect(pipeline_status).to be_loaded + end + + it 'only loads the status once' do + expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1) + expect(pipeline_status).to receive(:load_from_cache).exactly(1) + + pipeline_status.load_status + pipeline_status.load_status + end + end + + describe "#load_from_commit" do + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } + + it 'reads the status from the pipeline for the commit' do + pipeline_status.load_from_commit + + expect(pipeline_status.status).to eq('success') + expect(pipeline_status.sha).to eq(project.commit.sha) + end + + it "doesn't fail for an empty project" do + status_for_empty_commit = described_class.new(create(:empty_project)) + + status_for_empty_commit.load_status + + expect(status_for_empty_commit).to be_loaded + end + end + + describe "#store_in_cache", :redis do + it "sets the object in redis" do + pipeline_status.sha = '123456' + pipeline_status.status = 'failed' + + pipeline_status.store_in_cache + read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) } + + expect(read_sha).to eq('123456') + expect(read_status).to eq('failed') + end + end + + describe '#store_in_cache_if_needed', :redis do + it 'stores the state in the cache when the sha is the HEAD of the project' do + create(:ci_pipeline, :success, project: project, sha: project.commit.sha) + build_status = described_class.load_for_project(project) + + build_status.store_in_cache_if_needed + sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) } + + expect(sha).not_to be_nil + expect(status).not_to be_nil + end + + it "doesn't store the status in redis when the sha is not the head of the project" do + other_status = described_class.new(project, sha: "123456", status: "failed") + + other_status.store_in_cache_if_needed + sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) } + + expect(sha).to be_nil + expect(status).to be_nil + end + + it "deletes the cache if the repository doesn't have a head commit" do + empty_project = create(:empty_project) + Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) } + other_status = described_class.new(empty_project, sha: "123456", status: "failed") + + other_status.store_in_cache_if_needed + sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) } + + expect(sha).to be_nil + expect(status).to be_nil + end + end + + describe "with a status in redis", :redis do + let(:status) { 'success' } + let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' } + + before do + Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) } + end + + describe '#load_from_cache' do + it 'reads the status from redis' do + pipeline_status.load_from_cache + + expect(pipeline_status.sha).to eq(sha) + expect(pipeline_status.status).to eq(status) + end + end + + describe '#has_cache?' do + it 'knows the status is cached' do + expect(pipeline_status.has_cache?).to be_truthy + end + end + + describe '#delete_from_cache' do + it 'deletes values from redis' do + pipeline_status.delete_from_cache + + key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") } + + expect(key_exists).to be_falsy + end + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 32f9366a14c..4b449546a30 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -212,6 +212,25 @@ eos end end + describe '#latest_pipeline' do + let!(:first_pipeline) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: 'success') + end + let!(:second_pipeline) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: 'success') + end + + it 'returns latest pipeline' do + expect(commit.latest_pipeline).to eq second_pipeline + end + end + describe '#status' do context 'without ref argument' do before do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 545a11912e3..31ae0dce140 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -344,6 +344,46 @@ describe Issue, "Issuable" do end end + describe '.order_due_date_and_labels_priority' do + let(:project) { create(:empty_project) } + + def create_issue(milestone, labels) + create(:labeled_issue, milestone: milestone, labels: labels, project: project) + end + + it 'sorts issues in order of milestone due date, then label priority' do + first_priority = create(:label, project: project, priority: 1) + second_priority = create(:label, project: project, priority: 2) + no_priority = create(:label, project: project) + + first_milestone = create(:milestone, project: project, due_date: Time.now) + second_milestone = create(:milestone, project: project, due_date: Time.now + 1.month) + third_milestone = create(:milestone, project: project) + + # The issues here are ordered by label priority, to ensure that we don't + # accidentally just sort by creation date. + second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority]) + third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority]) + first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority]) + second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority]) + no_milestone_second_priority = create_issue(nil, [second_priority, no_priority]) + first_milestone_no_priority = create_issue(first_milestone, [no_priority]) + second_milestone_no_labels = create_issue(second_milestone, []) + third_milestone_no_priority = create_issue(third_milestone, [no_priority]) + + result = Issue.order_due_date_and_labels_priority + + expect(result).to eq([first_milestone_second_priority, + first_milestone_no_priority, + second_milestone_first_priority, + second_milestone_second_priority, + second_milestone_no_labels, + third_milestone_first_priority, + no_milestone_second_priority, + third_milestone_no_priority]) + end + end + describe '.order_labels_priority' do let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) } let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) } diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index ad703a6c8bb..68e4c0a522b 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -116,21 +116,41 @@ describe Milestone, 'Milestoneish' do end end + describe '#remaining_days' do + it 'shows 0 if no due date' do + milestone = build_stubbed(:milestone) + + expect(milestone.remaining_days).to eq(0) + end + + it 'shows 0 if expired' do + milestone = build_stubbed(:milestone, due_date: 2.days.ago) + + expect(milestone.remaining_days).to eq(0) + end + + it 'shows correct remaining days' do + milestone = build_stubbed(:milestone, due_date: 2.days.from_now) + + expect(milestone.remaining_days).to eq(2) + end + end + describe '#elapsed_days' do it 'shows 0 if no start_date set' do - milestone = build(:milestone) + milestone = build_stubbed(:milestone) expect(milestone.elapsed_days).to eq(0) end it 'shows 0 if start_date is a future' do - milestone = build(:milestone, start_date: Time.now + 2.days) + milestone = build_stubbed(:milestone, start_date: Time.now + 2.days) expect(milestone.elapsed_days).to eq(0) end it 'shows correct amount of days' do - milestone = build(:milestone, start_date: Time.now - 2.days) + milestone = build_stubbed(:milestone, start_date: Time.now - 2.days) expect(milestone.elapsed_days).to eq(2) end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 69906382545..255b584a85e 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do end end - describe '#min_relative_position' do - it 'returns maximum position' do - expect(issue.min_relative_position).to eq issue.relative_position - end - end - describe '#max_relative_position' do it 'returns maximum position' do expect(issue.max_relative_position).to eq issue1.relative_position @@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do expect(issue1.prev_relative_position).to eq issue.relative_position end - it 'returns minimum position if there is no issue above' do - expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION + it 'returns nil if there is no issue above' do + expect(issue.prev_relative_position).to eq nil end end @@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do expect(issue.next_relative_position).to eq issue1.relative_position end - it 'returns next position if there is no issue below' do - expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION + it 'returns nil if there is no issue below' do + expect(issue1.next_relative_position).to eq nil end end @@ -72,6 +66,34 @@ describe Issue, 'RelativePositioning' do end end + describe '#shift_after?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position - 1) + + expect(issue.shift_after?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position - 2) + + expect(issue.shift_after?).to be_falsey + end + end + + describe '#shift_before?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position + 1) + + expect(issue.shift_before?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position + 2) + + expect(issue.shift_before?).to be_falsey + end + end + describe '#move_between' do it 'positions issue between two other' do new_issue.move_between(issue, issue1) @@ -100,5 +122,83 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to be > issue.relative_position expect(issue.relative_position).to be < issue1.relative_position end + + it 'positions issues between other two if distance is 1' do + issue1.update relative_position: issue.relative_position + 1 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(issue.relative_position).to be < issue1.relative_position + end + + it 'positions issue in the middle of other two if distance is big enough' do + issue.update relative_position: 6000 + issue1.update relative_position: 10000 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(8000) + end + + it 'positions issue closer to the middle if we are at the very top' do + issue1.update relative_position: 6000 + + new_issue.move_between(nil, issue1) + + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE) + end + + it 'positions issue closer to the middle if we are at the very bottom' do + issue.update relative_position: 6000 + issue1.update relative_position: nil + + new_issue.move_between(issue, nil) + + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) + end + + it 'positions issue in the middle of other two if distance is not big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 400 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(250) + end + + it 'positions issue in the middle of other two is there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position) + end + + it 'uses rebalancing if there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue1, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + expect(issue.reload.relative_position).not_to eq(100) + end + + it 'positions issue right if we pass none-sequential parameters' do + issue.update relative_position: 99 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be(100) + end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index b4305e92812..9f0e7fbbe26 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -239,7 +239,7 @@ describe Environment, models: true do describe '#actions_for' do let(:deployment) { create(:deployment, environment: environment) } let(:pipeline) { deployment.deployable.pipeline } - let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )} + let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )} let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )} it 'returns a list of actions with matching environment' do diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index cacbab8bcb1..55b87d1c48a 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do end end + describe '.states_count' do + context 'when the projects have milestones' do + before do + create(:closed_milestone, title: 'Active Group Milestone', project: project3) + create(:active_milestone, title: 'Active Group Milestone', project: project1) + create(:active_milestone, title: 'Active Group Milestone', project: project2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project1) + create(:closed_milestone, title: 'Closed Group Milestone', project: project2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project3) + end + + it 'returns the quantity of global milestones in each possible state' do + expected_count = { opened: 1, closed: 2, all: 2 } + + count = GlobalMilestone.states_count(Project.all) + + expect(count).to eq(expected_count) + end + end + + context 'when the projects do not have milestones' do + before do + project1 + end + + it 'returns 0 as the quantity of global milestones in each state' do + expected_count = { opened: 0, closed: 0, all: 0 } + + count = GlobalMilestone.states_count(Project.all) + + expect(count).to eq(expected_count) + end + end + end + describe '#initialize' do let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } @@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do expect(global_milestone.safe_title).to eq('git-test') end end + + describe '#state' do + context 'when at least one milestone is active' do + it 'returns active' do + title = 'Active Group Milestone' + milestones = [ + create(:active_milestone, title: title), + create(:closed_milestone, title: title) + ] + global_milestone = GlobalMilestone.new(title, milestones) + + expect(global_milestone.state).to eq('active') + end + end + + context 'when all milestones are closed' do + it 'returns closed' do + title = 'Closed Group Milestone' + milestones = [ + create(:closed_milestone, title: title), + create(:closed_milestone, title: title) + ] + global_milestone = GlobalMilestone.new(title, milestones) + + expect(global_milestone.state).to eq('closed') + end + end + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bba9058f394..9ffcb88bafd 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,6 +22,21 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end + describe '#order_by_position_and_priority' do + let(:project) { create :empty_project } + let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } + let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } + let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) } + let!(:issue3) { create(:issue, project: project, relative_position: 100) } + let!(:issue4) { create(:issue, project: project, relative_position: 200) } + + it 'returns ordered list' do + expect(project.issues.order_by_position_and_priority). + to match [issue3, issue4, issue1, issue2] + end + end + describe '#to_reference' do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } @@ -620,4 +635,15 @@ describe Issue, models: true do end end end + + describe '#hook_attrs' do + let(:attrs_hash) { subject.hook_attrs } + + it 'includes time tracking attrs' do + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fcaf4c71182..24e7c1b17d9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -542,7 +542,7 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do - let(:attrs_hash) { subject.hook_attrs.to_h } + let(:attrs_hash) { subject.hook_attrs } [:source, :target].each do |key| describe "#{key} key" do @@ -558,6 +558,10 @@ describe MergeRequest, models: true do expect(attrs_hash).to include(:target) expect(attrs_hash).to include(:last_commit) expect(attrs_hash).to include(:work_in_progress) + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e120e21af06..ff1defcd32d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1916,4 +1916,15 @@ describe Project, models: true do end end end + + describe '#pipeline_status' do + let(:project) { create(:project) } + it 'builds a pipeline status' do + expect(project.pipeline_status).to be_a(Ci::PipelineStatus) + end + + it 'hase a loaded pipeline status' do + expect(project.pipeline_status).to be_loaded + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index adb5b538922..9da4140f3ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -210,22 +210,6 @@ describe User, models: true do end end end - - describe 'ghost users' do - it 'does not allow a non-blocked ghost user' do - user = build(:user, :ghost) - user.state = 'active' - - expect(user).to be_invalid - end - - it 'allows a blocked ghost user' do - user = build(:user, :ghost) - user.state = 'blocked' - - expect(user).to be_valid - end - end end describe "scopes" do @@ -713,8 +697,9 @@ describe User, models: true do describe '.search_with_secondary_emails' do delegate :search_with_secondary_emails, to: :described_class - let!(:user) { create(:user) } - let!(:email) { create(:email) } + let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) } + let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) } + let!(:email) { create(:email, user: another_user) } it 'returns users with a matching name' do expect(search_with_secondary_emails(user.name)).to eq([user]) diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 63acc0b68cd..02acdcb36df 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' describe BasePolicy, models: true do - let(:build) { Ci::Build.new } - describe '.class_for' do it 'detects policy class based on the subject ancestors' do - expect(described_class.class_for(build)).to eq(Ci::BuildPolicy) + expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) end it 'detects policy class for a presented subject' do - presentee = Ci::BuildPresenter.new(build) + presentee = Ci::BuildPresenter.new(Ci::Build.new) expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy) end + + it 'uses GlobalPolicy when :global is given' do + expect(described_class.class_for(:global)).to eq(GlobalPolicy) + end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 585449e62b6..a10d876ffad 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -178,7 +178,7 @@ describe API::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -193,7 +193,7 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -237,8 +237,8 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + context 'with project path containing a dot in URL' do + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params @@ -248,7 +248,7 @@ describe API::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -289,7 +289,7 @@ describe API::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -334,7 +334,7 @@ describe API::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { @@ -377,7 +377,7 @@ describe API::Commits, api: true do end end - context "multiple operations" do + describe 'multiple operations' do let(:message) { 'Multiple actions' } let!(:invalid_mo_params) do { diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index a89676fec93..988a57a80ea 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -436,7 +436,7 @@ describe API::Helpers, api: true do context 'current_user is present' do before do - expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new) end it 'does not raise an error' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 2fc11a3b782..e7738ca3034 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -153,6 +153,16 @@ describe API::Issues, api: true do expect(json_response.first['state']).to eq('opened') end + it 'returns unlabeled issues for "No Label" label' do + get api("/issues", user), labels: 'No Label' + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to be_empty + end + it 'returns an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) @@ -928,29 +938,34 @@ describe API::Issues, api: true do ]) end - context 'resolving issues in a merge request' do + context 'resolving discussions' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } + before do project.team << [user, :master] - post api("/projects/#{project.id}/issues", user), - title: 'New Issue', - merge_request_for_resolving_discussions: merge_request.iid - end - - it 'creates a new project issue' do - expect(response).to have_http_status(:created) end - it 'resolves the discussions in a merge request' do - discussion.first_note.reload + context 'resolving all discussions in a merge request' do + before do + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid + end - expect(discussion.resolved?).to be(true) + it_behaves_like 'creating an issue resolving discussions through the API' end - it 'assigns a description to the issue mentioning the merge request' do - expect(json_response['description']).to include(merge_request.to_reference) + context 'resolving a single discussion' do + before do + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid, + discussion_to_resolve: discussion.id + end + + it_behaves_like 'creating an issue resolving discussions through the API' end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index b4b23617498..c481b7e72b1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- require 'spec_helper' -describe API::Projects, api: true do - include ApiHelpers +describe API::Projects, :api do include Gitlab::CurrentSettings + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 15d458e0795..442b2df1952 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -39,6 +39,7 @@ describe API::Runner do expect(json_response['id']).to eq(runner.id) expect(json_response['token']).to eq(runner.token) expect(runner.run_untagged).to be true + expect(runner.token).not_to eq(registration_token) end context 'when project token is used' do @@ -49,6 +50,8 @@ describe API::Runner do expect(response).to have_http_status 201 expect(project.runners.size).to eq(1) + expect(Ci::Runner.first.token).not_to eq(registration_token) + expect(Ci::Runner.first.token).not_to eq(project.runners_token) end end end @@ -309,8 +312,8 @@ describe API::Runner do end let(:expected_variables) do - [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true }, - { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true }, + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true }, { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }] end @@ -434,9 +437,9 @@ describe API::Runner do context 'when triggered job is available' do let(:expected_variables) do - [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true }, - { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true }, - { 'key' => 'CI_BUILD_TRIGGERED', 'value' => 'true', 'public' => true }, + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true }, + { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true }, { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }, { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false }, { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }] diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index e298ef055e1..adba3a787aa 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params @@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 9948d1a9ea0..c879f37f50d 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -81,8 +81,8 @@ describe Ci::API::Builds do expect(runner.reload.platform).to eq("darwin") expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true } ) end @@ -182,9 +182,9 @@ describe Ci::API::Builds do expect(response).to have_http_status(201) expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, + { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true }, + { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }, diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 8719313783e..d50cdfdc2d6 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -18,6 +18,7 @@ describe Ci::API::Runners do it 'creates runner with default values' do expect(response).to have_http_status 201 expect(Ci::Runner.first.run_untagged).to be true + expect(Ci::Runner.first.token).not_to eq(registration_token) end end @@ -74,6 +75,8 @@ describe Ci::API::Runners do it 'creates runner' do expect(response).to have_http_status 201 expect(project.runners.size).to eq(1) + expect(Ci::Runner.first.token).not_to eq(registration_token) + expect(Ci::Runner.first.token).not_to eq(project.runners_token) end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 01baedc4761..22115c6566d 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -43,6 +43,32 @@ describe Boards::Issues::ListService, services: true do described_class.new(project, user, params).execute end + context 'issues are ordered by priority' do + it 'returns opened issues when list_id is missing' do + params = { board_id: board.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + + it 'returns closed issues when listing issues from Done' do + params = { board_id: board.id, id: done.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + end + + it 'returns opened issues that have label list applied when listing issues from a label list' do + params = { board_id: board.id, id: list1.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + end + end + context 'with list that does not belong to the board' do it 'raises an error' do list = create(:list) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 8459a3d8cfb..a969829a63e 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -200,7 +200,7 @@ describe Ci::CreatePipelineService, services: true do context 'with environment' do before do - config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + config = YAML.dump(deploy: { environment: { name: "review/$CI_COMMIT_REF_NAME" }, script: 'ls' }) stub_ci_pipeline_yaml_file(config) end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 18b964e2453..a883705bd45 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -104,16 +104,16 @@ describe CreateDeploymentService, services: true do context 'when variables are used' do let(:params) do { - environment: 'review-apps/$CI_BUILD_REF_NAME', + environment: 'review-apps/$CI_COMMIT_REF_NAME', ref: 'master', tag: false, sha: '97de212e80737a608d939f648d959671fb0a0142', options: { - name: 'review-apps/$CI_BUILD_REF_NAME', - url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com' + name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' }, variables: [ - { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' } + { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' } ] } end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 09807e5d35b..1dd53236fbd 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -8,24 +8,34 @@ describe Issues::BuildService, services: true do project.team << [user, :developer] end + context 'for a single discussion' do + describe '#execute' do + let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) } + let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done")]) } + let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) } + + it 'references the noteable title in the issue title' do + issue = service.execute + + expect(issue.title).to include('Hello world') + end + + it 'adds the note content to the description' do + issue = service.execute + + expect(issue.description).to include('Almost done') + end + end + end + context 'for discussions in a merge request' do let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } - let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute } - - def position_on_line(line_number) - Gitlab::Diff::Position.new( - old_path: "files/ruby/popen.rb", - new_path: "files/ruby/popen.rb", - old_line: nil, - new_line: line_number, - diff_refs: merge_request.diff_refs - ) - end + let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute } describe '#items_for_discussions' do it 'has an item for each discussion' do - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13)) - service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13) + service = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) service.execute @@ -34,7 +44,7 @@ describe Issues::BuildService, services: true do end describe '#item_for_discussion' do - let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) } + let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } it 'mentions the author of the note' do discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))]) @@ -47,11 +57,11 @@ describe Issues::BuildService, services: true do "with a blockquote\n"\ "> That has a quote\n"\ ">>>\n" - note_result = "This is a string\n"\ - "> with a blockquote\n"\ - "> > That has a quote\n" + note_result = " > This is a string\n"\ + " > > with a blockquote\n"\ + " > > > That has a quote\n" discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)]) - expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>") + expect(service.item_for_discussion(discussion)).to include(note_result) end end @@ -66,7 +76,7 @@ describe Issues::BuildService, services: true do it 'does not assign title when a title was given' do issue = described_class.new(project, user, - merge_request_for_resolving_discussions: merge_request, + merge_request_to_resolve_discussions_of: merge_request, title: 'What an issue').execute expect(issue.title).to eq('What an issue') @@ -74,7 +84,7 @@ describe Issues::BuildService, services: true do it 'does not assign description when a description was given' do issue = described_class.new(project, user, - merge_request_for_resolving_discussions: merge_request, + merge_request_to_resolve_discussions_of: merge_request, description: 'Fix at your earliest conveignance').execute expect(issue.description).to eq('Fix at your earliest conveignance') @@ -82,7 +92,7 @@ describe Issues::BuildService, services: true do describe 'with multiple discussions' do before do - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15) end it 'mentions all the authors in the description' do @@ -99,7 +109,7 @@ describe Issues::BuildService, services: true do end it 'mentions additional notes' do - create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, line_number: 15) expect(issue.description).to include('(+2 comments)') end @@ -112,7 +122,7 @@ describe Issues::BuildService, services: true do describe '#execute' do it 'mentions the merge request in the description' do - issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute + issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}") end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 6045d00ff09..776cbc4296b 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -140,46 +140,85 @@ describe Issues::CreateService, services: true do it_behaves_like 'new issuable record that supports slash commands' - context 'for a merge request' do + context 'resolving discussions' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } - let(:opts) { { merge_request_for_resolving_discussions: merge_request } } before do project.team << [user, :master] end - it 'resolves the discussion for the merge request' do - described_class.new(project, user, opts).execute - discussion.first_note.reload + describe 'for a single discussion' do + let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } } - expect(discussion.resolved?).to be(true) - end + it 'resolves the discussion' do + described_class.new(project, user, opts).execute + discussion.first_note.reload - it 'added a system note to the discussion' do - described_class.new(project, user, opts).execute + expect(discussion.resolved?).to be(true) + end - reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first + it 'added a system note to the discussion' do + described_class.new(project, user, opts).execute - expect(reloaded_discussion.last_note.system).to eq(true) - end + reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first + + expect(reloaded_discussion.last_note.system).to eq(true) + end + + it 'assigns the title and description for the issue' do + issue = described_class.new(project, user, opts).execute + + expect(issue.title).not_to be_nil + expect(issue.description).not_to be_nil + end - it 'assigns the title and description for the issue' do - issue = described_class.new(project, user, opts).execute + it 'can set nil explicitly to the title and description' do + issue = described_class.new(project, user, + merge_request_to_resolve_discussions_of: merge_request, + description: nil, + title: nil).execute - expect(issue.title).not_to be_nil - expect(issue.description).not_to be_nil + expect(issue.description).to be_nil + expect(issue.title).to be_nil + end end - it 'can set nil explicityly to the title and description' do - issue = described_class.new(project, user, - merge_request_for_resolving_discussions: merge_request, - description: nil, - title: nil).execute + describe 'for a merge request' do + let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } } + + it 'resolves the discussion' do + described_class.new(project, user, opts).execute + discussion.first_note.reload - expect(issue.description).to be_nil - expect(issue.title).to be_nil + expect(discussion.resolved?).to be(true) + end + + it 'added a system note to the discussion' do + described_class.new(project, user, opts).execute + + reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first + + expect(reloaded_discussion.last_note.system).to eq(true) + end + + it 'assigns the title and description for the issue' do + issue = described_class.new(project, user, opts).execute + + expect(issue.title).not_to be_nil + expect(issue.description).not_to be_nil + end + + it 'can set nil explicitly to the title and description' do + issue = described_class.new(project, user, + merge_request_to_resolve_discussions_of: merge_request, + description: nil, + title: nil).execute + + expect(issue.description).to be_nil + expect(issue.title).to be_nil + end end end diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb new file mode 100644 index 00000000000..6cc738aec08 --- /dev/null +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper.rb' + +class DummyService < Issues::BaseService + include ::Issues::ResolveDiscussions + + def initialize(*args) + super + filter_resolve_discussion_params + end +end + +describe DummyService, services: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + describe "for resolving discussions" do + let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, note: "Almost done")]) } + let(:merge_request) { discussion.noteable } + let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") } + + describe "#merge_request_for_resolving_discussion" do + let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } + + it "finds the merge request" do + expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request) + end + + it "only queries for the merge request once" do + fake_finder = double + fake_results = double + + expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1) + expect(fake_results).to receive(:find_by).exactly(1) + expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1) + + 2.times { service.merge_request_to_resolve_discussions_of } + end + end + + describe "#discussions_to_resolve" do + it "contains a single discussion when matching merge request and discussion are passed" do + service = described_class.new( + project, + user, + discussion_to_resolve: discussion.id, + merge_request_to_resolve_discussions_of: merge_request.iid + ) + # We need to compare discussion id's because the Discussion-objects are rebuilt + # which causes the object-id's not to be different. + discussion_ids = service.discussions_to_resolve.map(&:id) + + expect(discussion_ids).to contain_exactly(discussion.id) + end + + it "contains all discussions when only a merge request is passed" do + second_discussion = Discussion.new([create(:diff_note_on_merge_request, + noteable: merge_request, + project: merge_request.target_project, + line_number: 15)]) + service = described_class.new( + project, + user, + merge_request_to_resolve_discussions_of: merge_request.iid + ) + # We need to compare discussion id's because the Discussion-objects are rebuilt + # which causes the object-id's not to be different. + discussion_ids = service.discussions_to_resolve.map(&:id) + + expect(discussion_ids).to contain_exactly(discussion.id, second_discussion.id) + end + + it "contains only unresolved discussions" do + _second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved, + noteable: merge_request, + project: merge_request.target_project, + line_number: 15, + )]) + service = described_class.new( + project, + user, + merge_request_to_resolve_discussions_of: merge_request.iid + ) + # We need to compare discussion id's because the Discussion-objects are rebuilt + # which causes the object-id's not to be different. + discussion_ids = service.discussions_to_resolve.map(&:id) + + expect(discussion_ids).to contain_exactly(discussion.id) + end + + it "is empty when a discussion and another merge request are passed" do + service = described_class.new( + project, + user, + discussion_to_resolve: discussion.id, + merge_request_to_resolve_discussions_of: other_merge_request.iid + ) + + expect(service.discussions_to_resolve).to be_empty + end + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index ebbaea4e59a..82a4ec3f581 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -146,16 +146,6 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end - it "emails the note author if they've opted into notifications about their activity" do - add_users_with_subscription(note.project, issue) - note.author.notified_of_own_activity = true - reset_delivered_emails! - - notification.new_note(note) - - should_email(note.author) - end - it 'filters out "mentioned in" notes' do mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author) @@ -486,20 +476,6 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end - it "emails the author if they've opted into notifications about their activity" do - issue.author.notified_of_own_activity = true - - notification.new_issue(issue, issue.author) - - should_email(issue.author) - end - - it "doesn't email the author if they haven't opted into notifications about their activity" do - notification.new_issue(issue, issue.author) - - should_not_email(issue.author) - end - it "emails subscribers of the issue's labels" do user_1 = create(:user) user_2 = create(:user) @@ -689,19 +665,6 @@ describe NotificationService, services: true do should_email(subscriber_to_label_2) end - it "emails the current user if they've opted into notifications about their activity" do - subscriber_to_label_2.notified_of_own_activity = true - notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) - - should_email(subscriber_to_label_2) - end - - it "doesn't email the current user if they haven't opted into notifications about their activity" do - notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) - - should_not_email(subscriber_to_label_2) - end - it "doesn't send email to anyone but subscribers of the given labels" do notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) @@ -855,20 +818,6 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end - it "emails the author if they've opted into notifications about their activity" do - merge_request.author.notified_of_own_activity = true - - notification.new_merge_request(merge_request, merge_request.author) - - should_email(merge_request.author) - end - - it "doesn't email the author if they haven't opted into notifications about their activity" do - notification.new_merge_request(merge_request, merge_request.author) - - should_not_email(merge_request.author) - end - it "emails subscribers of the merge request's labels" do user_1 = create(:user) user_2 = create(:user) @@ -1064,14 +1013,6 @@ describe NotificationService, services: true do should_not_email(@u_watcher) end - it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do - merge_request.merge_when_pipeline_succeeds = false - @u_watcher.notified_of_own_activity = true - notification.merge_mr(merge_request, @u_watcher) - - should_email(@u_watcher) - end - it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index a8395cb48ea..3645b73b039 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -298,6 +298,10 @@ describe TodoService, services: true do expect(second_todo.reload.state?(new_state)).to be true end + it 'returns the updated ids' do + expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id]) + end + describe 'cached counts' do it 'updates when todos change' do expect(john_doe.todos.where(state: new_state).count).to eq(0) @@ -706,7 +710,7 @@ describe TodoService, services: true do should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE) end end - + describe '#mark_todo' do it 'creates a todo from a merge request' do service.mark_todo(mr_unassigned, author) @@ -779,29 +783,27 @@ describe TodoService, services: true do .to change { todo.reload.state }.from('pending').to('done') end - it 'returns the number of updated todos' do # Needed on API + it 'returns the ids of updated todos' do # Needed on API todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1) + expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id]) end context 'when some of the todos are done already' do - before do - create(:todo, :mentioned, user: john_doe, target: issue, project: project) - create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) - end + let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) } + let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) } - it 'returns the number of those still pending' do + it 'returns the ids of those still pending' do TodoService.new.mark_pending_todos_as_done(issue, john_doe) - expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1) + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id]) end - it 'returns 0 if all are done' do + it 'returns an empty array if all are done' do TodoService.new.mark_pending_todos_as_done(issue, john_doe) TodoService.new.mark_pending_todos_as_done(another_issue, john_doe) - expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0) + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([]) end end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index b507d38f472..ac2c89b3ff9 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -15,9 +15,9 @@ module SimpleCovEnv def configure_job SimpleCov.configure do - if ENV['CI_BUILD_NAME'] - coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}" - command_name ENV['CI_BUILD_NAME'] + if ENV['CI_JOB_NAME'] + coverage_dir "coverage/#{ENV['CI_JOB_NAME']}" + command_name ENV['CI_JOB_NAME'] end if ENV['CI'] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fda7c63cdb..ceb3209331f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,14 +43,27 @@ RSpec.configure do |config| config.include ActiveSupport::Testing::TimeHelpers config.include StubGitlabCalls config.include StubGitlabData + config.include ApiHelpers, :api config.infer_spec_type_from_file_location! + + config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata| + metadata[:api] = true + end + config.raise_errors_for_deprecations! config.before(:suite) do TestEnv.init end + if ENV['CI'] + # Retry only on feature specs that use JS + config.around :each, :js do |ex| + ex.run_with_retry retry: 3 + end + end + config.around(:each, :caching) do |example| caching_store = Rails.cache Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching] diff --git a/spec/support/api/issues_resolving_discussions_shared_examples.rb b/spec/support/api/issues_resolving_discussions_shared_examples.rb new file mode 100644 index 00000000000..d26d279363c --- /dev/null +++ b/spec/support/api/issues_resolving_discussions_shared_examples.rb @@ -0,0 +1,15 @@ +shared_examples 'creating an issue resolving discussions through the API' do + it 'creates a new project issue' do + expect(response).to have_http_status(:created) + end + + it 'resolves the discussions in a merge request' do + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'assigns a description to the issue mentioning the merge request' do + expect(json_response['description']).to include(merge_request.to_reference) + end +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index ae6e708cf87..35d1e1cfc7d 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -49,8 +49,4 @@ module ApiHelpers '' end end - - def json_response - @_json_response ||= JSON.parse(response.body) - end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 62740ec29fd..aa14709bc9c 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,6 +1,7 @@ require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' +require 'capybara-screenshot/rspec' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 @@ -21,12 +22,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/rspec' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| config.before(:suite) do diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb new file mode 100644 index 00000000000..4a946995f84 --- /dev/null +++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb @@ -0,0 +1,41 @@ +shared_examples 'creating an issue for a discussion' do + it 'shows an issue with the title filled in' do + title_field = page.find_field('issue[title]') + + expect(title_field.value).to include(merge_request.title) + end + + it 'has a mention of the discussion in the description' do + description_field = page.find_field('issue[description]') + + expect(description_field.value).to include(discussion.first_note.note) + end + + it 'can create a new issue for the project' do + expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1) + end + + it 'resolves the discussion in the merge request' do + click_button 'Submit issue' + + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + + it 'shows a flash messaage after resolving a discussion' do + click_button 'Submit issue' + + page.within '.flash-notice' do + # Only check for the word 'Resolved' since the spec might have resolved + # multiple discussions + expect(page).to have_content('Resolved') + end + end + + it 'has a hidden field for the merge request' do + merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false) + + expect(merge_request_field.value).to eq(merge_request.iid.to_s) + end +end diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb new file mode 100644 index 00000000000..e8d2ef2d7f0 --- /dev/null +++ b/spec/support/json_response_helpers.rb @@ -0,0 +1,9 @@ +shared_context 'JSON response' do + let(:json_response) { JSON.parse(response.body) } +end + +RSpec.configure do |config| + config.include_context 'JSON response', type: :controller + config.include_context 'JSON response', type: :request + config.include_context 'JSON response', :api +end diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 07f81e9c4f3..f55fee28ff9 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git") TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git") module SeedHelper - GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze + GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze def ensure_seeds if File.exist?(SEED_REPOSITORY_PATH) @@ -25,7 +25,7 @@ module SeedHelper end def create_bare_seeds - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}), chdir: SEED_REPOSITORY_PATH, out: '/dev/null', err: '/dev/null') @@ -45,7 +45,7 @@ module SeedHelper system(git_env, *%w(git branch -t feature origin/feature), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') - system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') end diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index f2919f20e85..8bc344bfbf6 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -25,7 +25,7 @@ describe 'projects/commit/_commit_box.html.haml' do render - expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed") + expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed") end context 'viewing a commit' do |