diff options
Diffstat (limited to 'spec')
140 files changed, 3518 insertions, 1435 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/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb new file mode 100644 index 00000000000..84a1ce773a1 --- /dev/null +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Admin::ApplicationSettingsController do + include StubENV + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + + describe 'PATCH #update' do + it 'updates the default_project_visibility for string value' do + patch :update, application_setting: { default_project_visibility: "20" } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC + end + + it 'falls back to default with default_project_visibility setting is omitted' do + patch :update, application_setting: {} + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE + end + 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/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 6ceaf96f78f..57a921e3676 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -87,6 +87,12 @@ describe Projects::IssuesController do end describe 'GET #new' do + it 'redirects to signin if not logged in' do + get :new, namespace_id: project.namespace, project_id: project + + expect(response).to redirect_to(new_user_session_path) + end + context 'internal issue tracker' do before do sign_in(user) @@ -121,6 +127,11 @@ describe Projects::IssuesController do end context 'external issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end + it 'redirects to the external issue tracker' do external = double(new_issue_path: 'https://example.com/issues/new') allow(project).to receive(:external_issue_tracker).and_return(external) @@ -141,6 +152,24 @@ describe Projects::IssuesController do it_behaves_like 'update invalid issuable', Issue + context 'changing the assignee' do + it 'limits the attributes exposed on the assignee' do + assignee = create(:user) + project.add_developer(assignee) + + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: issue.iid, + issue: { assignee_id: assignee.id }, + format: :json + body = JSON.parse(response.body) + + expect(body['assignee'].keys) + .to match_array(%w(name username avatar_url)) + end + end + context 'when moving issue to another private project' do let(:another_project) { create(:empty_project, :private) } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 250d64f7055..c310d830e81 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -203,6 +203,24 @@ describe Projects::MergeRequestsController do end describe 'PUT update' do + context 'changing the assignee' do + it 'limits the attributes exposed on the assignee' do + assignee = create(:user) + project.add_developer(assignee) + + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + merge_request: { assignee_id: assignee.id }, + format: :json + body = JSON.parse(response.body) + + expect(body['assignee'].keys) + .to match_array(%w(name username avatar_url)) + end + end + context 'there is no source project' do let(:project) { create(:project) } let(:fork_project) { create(:forked_project_with_submodules) } diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index e3f3b4fe8eb..1ecfe48475c 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -35,7 +35,7 @@ describe Projects::VariablesController do context 'updating a variable with valid characters' do before do - variable.gl_project_id = project.id + variable.project_id = project.id project.variables << variable 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/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb index 3372e5ab685..6712dd5d82e 100644 --- a/spec/factories/ci/runner_projects.rb +++ b/spec/factories/ci/runner_projects.rb @@ -1,6 +1,6 @@ FactoryGirl.define do factory :ci_runner_project, class: Ci::RunnerProject do runner_id 1 - gl_project_id 1 + project_id 1 end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index ae0bbbd6aeb..21487541507 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -4,6 +4,7 @@ FactoryGirl.define do author association :source_project, :repository, factory: :project target_project { source_project } + project { target_project } # $ git log --pretty=oneline feature..master # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index de42ab81fac..03daab12c8f 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do visit admin_application_settings_path end + scenario 'Change visibility settings' do + choose "application_setting_default_project_visibility_20" + click_button 'Save' + + expect(page).to have_content "Application settings saved successfully" + end + scenario 'Change application settings' do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' @@ -26,7 +33,7 @@ feature 'Admin updates settings', feature: true do fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' - page.check('Notify only broken builds') + page.check('Notify only broken pipelines') check_all_events click_on 'Save' @@ -50,7 +57,6 @@ feature 'Admin updates settings', feature: true do page.check('Note') page.check('Issue') page.check('Merge request') - page.check('Build') page.check('Pipeline') end end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index a7c22615b89..58b14e09740 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe "Dashboard Issues Feed", feature: true do describe "GET /issues" do - let!(:user) { create(:user) } + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:project1) { create(:project) } let!(:project2) { create(:project) } @@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do end context "issue with basic fields" do - let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') } + let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') } it "renders issue fields" do visit issues_dashboard_path(:atom, private_token: user.private_token) @@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true do entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") expect(entry).to be_present - expect(entry).to have_selector('author email', text: issue2.author_email) - expect(entry).to have_selector('assignee email', text: issue2.author_email) + expect(entry).to have_selector('author email', text: issue2.author_public_email) + expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email) expect(entry).not_to have_selector('labels') expect(entry).not_to have_selector('milestone') expect(entry).to have_selector('description', text: issue2.description) @@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do context "issue with label and milestone" do let!(:milestone1) { create(:milestone, project: project1, title: 'v1') } let!(:label1) { create(:label, project: project1, title: 'label1') } - let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) } + let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) } before do issue1.labels << label1 @@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true do entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") expect(entry).to be_present - expect(entry).to have_selector('author email', text: issue1.author_email) - expect(entry).to have_selector('assignee email', text: issue1.author_email) + expect(entry).to have_selector('author email', text: issue1.author_public_email) + expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email) expect(entry).to have_selector('labels label', text: label1.title) expect(entry).to have_selector('milestone', text: milestone1.title) expect(entry).not_to have_selector('description') diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index a01a050a013..b3903ec2faf 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe 'Issues Feed', feature: true do describe 'GET /issues' do - let!(:user) { create(:user) } + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:group) { create(:group) } let!(:project) { create(:project) } - let!(:issue) { create(:issue, author: user, project: project) } + let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) } before do project.team << [user, :developer] @@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true do expect(response_headers['Content-Type']). to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") - expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('author email', text: issue.author_public_email) + expect(body).to have_selector('assignee email', text: issue.author_public_email) expect(body).to have_selector('entry summary', text: issue.title) end end @@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true do expect(response_headers['Content-Type']). to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") - expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('author email', text: issue.author_public_email) + expect(body).to have_selector('assignee email', text: issue.author_public_email) expect(body).to have_selector('entry summary', text: issue.title) end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index f7f2d883d2f..d17a418b8c3 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -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/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_spec.rb b/spec/features/groups/group_name_toggle_spec.rb new file mode 100644 index 00000000000..8528718a2f7 --- /dev/null +++ b/spec/features/groups/group_name_toggle_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Group name toggle', feature: true, 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/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/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_spec.rb b/spec/features/issues_spec.rb index 8c54259f475..6f831ea4b67 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -6,7 +6,7 @@ describe 'Issues', feature: true do include SortingHelper include WaitForAjax - let(:project) { create(:project) } + let(:project) { create(:project, :public) } before do login_as :user @@ -565,6 +565,24 @@ describe 'Issues', feature: true do end describe 'new issue' do + context 'by unauthenticated user' do + before do + logout + end + + it 'redirects to signin then back to new issue after signin' do + visit namespace_project_issues_path(project.namespace, project) + + click_link 'New issue' + + expect(current_path).to eq new_user_session_path + + login_as :user + + expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project) + end + end + context 'dropzone upload file', js: true do before do visit new_namespace_project_issue_path(project.namespace, project) diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 73c5ef31edc..18833ba7266 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -60,9 +60,6 @@ feature 'Merge request created from fork' do expect(page).to have_content pipeline.status expect(page).to have_content pipeline.id end - - expect(page.find('a.btn-remove')[:href]) - .to include fork_project.path_with_namespace end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 6fed1568fcf..14511707af4 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -49,6 +49,26 @@ feature 'Merge requests filter clear button', feature: true, js: true do end end + context 'when multiple label filters have been applied' do + let!(:label) { create(:label, project: project, name: 'Frontend') } + let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") } + + before do + visit_merge_requests(project) + init_label_search + end + + it 'filters bug label' do + filtered_search.set('~bug') + + filter_dropdown.find('.filter-dropdown-item', text: bug.title).click + init_label_search + + expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible + expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + end + end + context 'when a text search has been conducted' do it 'resets the text search filter' do visit_merge_requests(project, search: 'Bug') diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 2f3c3e45ae6..a1f4eb2688b 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it 'changes target_branch in new merge_request' do visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) - click_button "Compare branches and continue" fill_in "merge_request_title", with: 'My brand new feature' fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" 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/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index 03d08c12612..5686868a0c4 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' feature 'New blob creation', feature: true, js: true do include WaitForAjax + include TargetBranchHelpers given(:user) { create(:user) } given(:role) { :developer } @@ -20,19 +21,6 @@ feature 'New blob creation', feature: true, js: true do 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 @@ -53,12 +41,12 @@ feature 'New blob creation', feature: true, js: true do context 'with different target branch' do background do edit_file - select_branch_index(0) + select_branch('feature') commit_file end scenario 'creates the blob in the different branch' do - expect(page).to have_content 'test' + expect(page).to have_content 'feature' expect(page).to have_content 'successfully created' end end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index d26a0caf036..8e0306ce83b 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -17,6 +17,14 @@ describe 'Branches', feature: true do repository.branches { |branch| expect(page).to have_content("#{branch.name}") } expect(page).to have_content("Protected branches can be managed in project settings") end + + it 'avoids a N+1 query in branches index' do + control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count + + %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') } + + expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count) + end end describe 'Find branches' do 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/compare_spec.rb b/spec/features/projects/compare_spec.rb index 43eb4000e58..030043d14aa 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -26,6 +26,14 @@ describe "Compare", js: true do click_button "Compare" expect(page).to have_content "Commits" end + + it "filters branches" do + select_using_dropdown("from", "wip") + + find(".js-compare-from-dropdown .compare-dropdown-toggle").click + + expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3) + end end describe "tags" do diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 8b302a6aa23..4c28205da9b 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -8,7 +8,7 @@ feature 'Project group links', feature: true, js: true do let!(:group) { create(:group) } background do - project.team << [master, :master] + project.add_master(master) login_as(master) end @@ -29,4 +29,26 @@ feature 'Project group links', feature: true, js: true do end end end + + context 'nested group project' do + let!(:nested_group) { create(:group, parent: group) } + let!(:another_group) { create(:group) } + let!(:project) { create(:project, namespace: nested_group) } + + background do + group.add_master(master) + another_group.add_master(master) + end + + it 'does not show ancestors' do + visit namespace_project_settings_members_path(project.namespace, project) + + click_link 'Search for a group' + + page.within '.select2-drop' do + expect(page).to have_content(another_group.name) + expect(page).not_to have_content(group.name) + end + end + end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 20cdfbae24f..399c1d478c5 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz 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/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 22bf1bfbdf0..162056671e0 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do end it 'indicates that pipeline can be canceled' do - expect(page).to have_link('Cancel') + expect(page).to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-running') end context 'when canceling' do - before { click_link('Cancel') } + before do + find('.js-pipelines-cancel-button').click + wait_for_vue_resource + end it 'indicated that pipelines was canceled' do - expect(page).not_to have_link('Cancel') + expect(page).not_to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-canceled') end end @@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do end it 'indicates that pipeline can be retried' do - expect(page).to have_link('Retry') + expect(page).to have_selector('.js-pipelines-retry-button') expect(page).to have_selector('.ci-failed') end context 'when retrying' do - before { click_link('Retry') } + before do + find('.js-pipelines-retry-button').click + wait_for_vue_resource + end it 'shows running pipeline that is not retryable' do - expect(page).not_to have_link('Retry') + expect(page).not_to have_selector('.js-pipelines-retry-button') expect(page).to have_selector('.ci-running') end end @@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do it 'has link to the manual action' do find('.js-pipeline-dropdown-manual-actions').click - expect(page).to have_link('manual build') + expect(page).to have_button('manual build') end context 'when manual action was played' do before do find('.js-pipeline-dropdown-manual-actions').click - click_link('manual build') + click_button('manual build') end it 'enqueues manual action job' do - expect(manual.reload).to be_pending + expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled') end end end @@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do before { visit_project_pipelines } it 'is cancelable' do - expect(page).to have_link('Cancel') + expect(page).to have_selector('.js-pipelines-cancel-button') end it 'has pipeline running' do @@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do end context 'when canceling' do - before { click_link('Cancel') } + before { find('.js-pipelines-cancel-button').trigger('click') } it 'indicates that pipeline was canceled' do - expect(page).not_to have_link('Cancel') + expect(page).not_to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-canceled') end end @@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do end it 'is not retryable' do - expect(page).not_to have_link('Retry') + expect(page).not_to have_selector('.js-pipelines-retry-button') end it 'has failed pipeline' do diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 16541f51d98..c0a4a1e4bf5 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do background do service.fields - service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7) + service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7) project.team << [user, :master] login_as(user) end @@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do expect(page.find_field("service_merge_request_channel").value).to have_content '3' expect(page.find_field("service_note_channel").value).to have_content '4' expect(page.find_field("service_tag_push_channel").value).to have_content '5' - expect(page.find_field("service_build_channel").value).to have_content '6' + expect(page.find_field("service_pipeline_channel").value).to have_content '6' expect(page.find_field("service_wiki_page_channel").value).to have_content '7' end end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 6815039d5ed..321af416c91 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -62,4 +62,27 @@ feature 'Project settings > Merge Requests', feature: true, js: true do expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end + + describe 'Checkbox to enable merge request link' do + before do + visit edit_project_path(project) + end + + scenario 'is initially checked' do + checkbox = find_field('project_printing_merge_request_link_enabled') + expect(checkbox).to be_checked + end + + scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do + uncheck('project_printing_merge_request_link_enabled') + click_on('Save') + + # Wait for save to complete and page to reload + checkbox = find_field('project_printing_merge_request_link_enabled') + expect(checkbox).not_to be_checked + + project.reload + expect(project.printing_merge_request_link_enabled).to be(false) + end + end end diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb new file mode 100644 index 00000000000..2065abfb248 --- /dev/null +++ b/spec/features/projects/user_create_dir_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +feature 'New directory creation', feature: true, js: true do + include WaitForAjax + include TargetBranchHelpers + + given(:user) { create(:user) } + given(:role) { :developer } + given(:project) { create(:project) } + + background do + login_as(user) + project.team << [user, role] + visit namespace_project_tree_path(project.namespace, project, 'master') + open_new_directory_modal + fill_in 'dir_name', with: 'new_directory' + end + + def open_new_directory_modal + first('.add-to-tree').click + click_link 'New directory' + end + + def create_directory + click_button 'Create directory' + end + + context 'with default target branch' do + background do + create_directory + end + + scenario 'creates the directory in the default branch' do + expect(page).to have_content 'master' + expect(page).to have_content 'The directory has been successfully created' + expect(page).to have_content 'new_directory' + end + end + + context 'with different target branch' do + background do + select_branch('feature') + create_directory + end + + scenario 'creates the directory in the different branch' do + expect(page).to have_content 'feature' + expect(page).to have_content 'The directory has been successfully created' + end + end + + context 'with a new target branch' do + given(:new_branch_name) { 'new-feature' } + + background do + create_new_branch(new_branch_name) + create_directory + end + + scenario 'creates the directory in the new branch' do + expect(page).to have_content new_branch_name + expect(page).to have_content 'The directory has been successfully created' + end + + scenario 'redirects to the merge request' 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 directory' + expect(current_path).to eq(new_namespace_project_merge_request_path(project.namespace, project)) + end + end +end diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb new file mode 100644 index 00000000000..6825b95c8aa --- /dev/null +++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Projects > Wiki > User views Git access wiki page', :feature do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:wiki_page) do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + before do + login_as(user) + end + + scenario 'Visit Wiki Page Current Commit' do + visit namespace_project_wiki_path(project.namespace, project, wiki_page) + + click_link 'Clone repository' + expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}") + expect(page).to have_text(project.wiki.http_url_to_repo(user)) + end +end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index f842d14fa96..aedc0333cb9 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do context 'in the user namespace' do let(:project) { create(:project, namespace: user.namespace) } - scenario 'the home page' do - click_link 'Edit' - - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Save changes' - - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + context 'the home page' do + scenario 'success when the wiki content is not empty' do + click_link 'Edit' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Save changes' + + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'failure when the wiki content is empty' do + click_link 'Edit' + + fill_in :wiki_content, with: '' + click_button 'Save changes' + + expect(page).to have_selector('.wiki-form') + expect(page).to have_content('Edit Page') + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content('Content can\'t be blank') + expect(find('textarea#wiki_content').value).to eq '' + 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_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb index 29d2c244720..555f84c4772 100644 --- a/spec/features/tags/master_views_tags_spec.rb +++ b/spec/features/tags/master_views_tags_spec.rb @@ -27,10 +27,20 @@ feature 'Master views tags', feature: true do context 'when project has tags' do let(:project) { create(:project, namespace: user.namespace) } + let(:repository) { project.repository } + before do visit namespace_project_tags_path(project.namespace, project) end + scenario 'avoids a N+1 query in branches index' do + control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_tags_path(project.namespace, project) }.count + + %w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') } + + expect { visit namespace_project_tags_path(project.namespace, project) }.not_to exceed_query_limit(control_count) + end + scenario 'views the tags list page' do expect(page).to have_content 'v1.0.0' end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 5c2df949ac5..850020109d4 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -31,7 +31,7 @@ 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 @@ -184,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/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 68b20a1e4fc..3cb809d42b5 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -47,4 +47,58 @@ describe MilestonesHelper do end end end + + describe '#milestone_remaining_days' do + around do |example| + Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } + end + + context 'when less than 31 days remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } + + it 'returns days remaining' do + expect(milestone_remaining).to eq("<strong>12</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.utc)) } + + 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).utc)) } + + 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.utc)) } + + 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.utc)) } + + 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.utc)) } + + 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..21e0e74e008 --- /dev/null +++ b/spec/helpers/todos_helper_spec.rb @@ -0,0 +1,57 @@ +require "spec_helper" + +describe TodosHelper do + include GitlabRoutingHelper + + describe '#todo_target_path' do + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, target_project: project, source_project: project) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note_on_issue, noteable: issue, project: project) } + + let(:mr_todo) { build(:todo, project: project, target: merge_request) } + let(:issue_todo) { build(:todo, project: project, target: issue) } + let(:note_todo) { build(:todo, project: project, target: issue, note: note) } + let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) } + + it 'returns correct path to the todo MR' do + expect(todo_target_path(mr_todo)). + to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}") + end + + it 'returns correct path to the todo issue' do + expect(todo_target_path(issue_todo)). + to eq("/#{project.full_path}/issues/#{issue.iid}") + end + + it 'returns correct path to the todo note' do + expect(todo_target_path(note_todo)). + to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}") + end + + it 'returns correct path to build_todo MR when pipeline failed' do + expect(todo_target_path(build_failed_todo)). + to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines") + end + end + + 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 0a6e042b700..ea7753c7a1d 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -287,6 +287,20 @@ import AwardsHandler from '~/awards_handler'; done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); + + it('should disregard invalid frequently used emoji that are being attempted to be added', function() { + awardsHandler.addEmojiToFrequentlyUsedList('8ball'); + awardsHandler.addEmojiToFrequentlyUsedList('invalid_emoji'); + awardsHandler.addEmojiToFrequentlyUsedList('grinning'); + + expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']); + }); + + it('should disregard invalid frequently used emoji already set in cookie', function() { + Cookies.set('frequently_used_emojis', '8ball,invalid_emoji,grinning'); + + expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']); + }); }); }); }).call(window); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js new file mode 100644 index 00000000000..47baf83512f --- /dev/null +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -0,0 +1,93 @@ +/* global BoardService */ +import Vue from 'vue'; +import '~/boards/stores/boards_store'; +import boardBlankState from '~/boards/components/board_blank_state'; +import './mock_data'; + +describe('Boards blank state', () => { + let vm; + let fail = false; + + beforeEach((done) => { + const Comp = Vue.extend(boardBlankState); + + gl.issueBoards.BoardsStore.create(); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + + spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => { + if (fail) { + reject(); + } else { + resolve({ + json() { + return [{ + id: 1, + title: 'To Do', + label: { id: 1 }, + }, { + id: 2, + title: 'Doing', + label: { id: 2 }, + }]; + }, + }); + } + })); + + vm = new Comp(); + + setTimeout(() => { + vm.$mount(); + done(); + }); + }); + + it('renders pre-defined labels', () => { + expect( + vm.$el.querySelectorAll('.board-blank-state-list li').length, + ).toBe(2); + + expect( + vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim(), + ).toEqual('To Do'); + + expect( + vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim(), + ).toEqual('Doing'); + }); + + it('clears blank state', (done) => { + vm.$el.querySelector('.btn-default').click(); + + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy(); + + done(); + }); + }); + + it('creates pre-defined labels', (done) => { + vm.$el.querySelector('.btn-create').click(); + + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do'); + expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing'); + + done(); + }); + }); + + it('resets the store if request fails', (done) => { + fail = true; + + vm.$el.querySelector('.btn-create').click(); + + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy(); + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + + done(); + }); + }); +}); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index fe7f3d2e9c4..549c7af8ea8 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -17,7 +17,7 @@ describe('Build', () => { spyOn($, 'ajax'); }); - describe('constructor', () => { + describe('class constructor', () => { beforeEach(() => { jasmine.clock().install(); }); diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js index 188908d66bd..82b00b4c1ec 100644 --- a/spec/javascripts/commit/pipelines/mock_data.js +++ b/spec/javascripts/commit/pipelines/mock_data.js @@ -1,5 +1,4 @@ -/* eslint-disable no-unused-vars */ -const pipeline = { +export default { id: 73, user: { name: 'Administrator', @@ -88,5 +87,3 @@ const pipeline = { created_at: '2017-01-16T17:13:59.800Z', updated_at: '2017-01-25T00:00:17.132Z', }; - -module.exports = pipeline; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index f09c57978a1..75efcc06585 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,11 +1,6 @@ -/* global pipeline, Vue */ - -require('~/flash'); -require('~/commit/pipelines/pipelines_store'); -require('~/commit/pipelines/pipelines_service'); -require('~/commit/pipelines/pipelines_table'); -require('~/vue_shared/vue_resource_interceptor'); -const pipeline = require('./mock_data'); +import Vue from 'vue'; +import PipelinesTable from '~/commit/pipelines/pipelines_table'; +import pipeline from './mock_data'; describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures('static/pipelines_table.html.raw'); @@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => { }); it('should render the empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ + const component = new PipelinesTable({ el: document.querySelector('#commit-pipeline-table-view'), }); @@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => { }); it('should render a table with the received pipelines', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ + const component = new PipelinesTable({ el: document.querySelector('#commit-pipeline-table-view'), }); @@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => { }); it('should render empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ + const component = new PipelinesTable({ el: document.querySelector('#commit-pipeline-table-view'), }); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js b/spec/javascripts/commit/pipelines/pipelines_store_spec.js deleted file mode 100644 index 4182a4b28ff..00000000000 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js +++ /dev/null @@ -1,30 +0,0 @@ -const PipelinesStore = require('~/commit/pipelines/pipelines_store'); - -describe('Store', () => { - let store; - - beforeEach(() => { - store = new PipelinesStore(); - }); - - it('should start with a blank state', () => { - expect(store.state.pipelines.length).toBe(0); - }); - - it('should store an array of pipelines', () => { - const pipelines = [ - { - id: '1', - name: 'pipeline', - }, - { - id: '2', - name: 'pipeline_2', - }, - ]; - - store.storePipelines(pipelines); - - expect(store.state.pipelines.length).toBe(pipelines.length); - }); -}); 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 ae9c263d1d7..113161c21c6 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -246,5 +246,17 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); }); }); + + describe('toggleInputContainerFocus', () => { + it('toggles on focus', () => { + input.focus(); + expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true); + }); + + it('toggles on blur', () => { + input.blur(); + expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false); + }); + }); }); })(); diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index 9b44b25980c..b2b46640e5b 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -43,6 +43,11 @@ const emojiFixtureMap = { moji: '5️⃣', unicodeVersion: '3.0', }, + grey_question: { + name: 'grey_question', + moji: '❔', + unicodeVersion: '6.0', + }, }; function markupToDomElement(markup) { @@ -153,6 +158,37 @@ describe('gl_emoji', () => { }, ); }); + + it('question mark when invalid emoji name given', () => { + const name = 'invalid_emoji'; + const emojiKey = 'grey_question'; + const markup = glEmojiTag(name); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + ); + }); + + it('question mark with image fallback when invalid emoji name given', () => { + const name = 'invalid_emoji'; + const emojiKey = 'grey_question'; + const markup = glEmojiTag(name, { + forceFallback: true, + }); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + { + forceFallback: true, + }, + ); + }); }); describe('isFlagEmoji', () => { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 8d25500b9fd..aabc8bea12f 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -136,6 +136,21 @@ describe('Issue', function() { expectErrorMessage(); expect($('.issue_counter')).toHaveText(1); }); + + it('updates counter', () => { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expectPendingRequest(req, $btnClose); + req.success({ + id: 34 + }); + }); + + expect($('.issue_counter')).toHaveText(1); + $('.issue_counter').text('1,001'); + expect($('.issue_counter').text()).toEqual('1,001'); + $btnClose.trigger('click'); + expect($('.issue_counter').text()).toEqual('1,000'); + }); }); describe('reopen issue', function() { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index fae462561e9..5cdb6473eda 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -32,10 +32,11 @@ testsContext.keys().forEach(function (path) { } }); -// workaround: include all source files to find files with 0% coverage -// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 -describe('Uncovered files', function () { - // the following files throw errors because of undefined variables +// if we're generating coverage reports, make sure to include all files so +// that we can catch files with 0% coverage +// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 +if (process.env.BABEL_ENV === 'coverage') { + // exempt these files from the coverage report const troubleMakers = [ './blob_edit/blob_edit_bundle.js', './cycle_analytics/components/stage_plan_component.js', @@ -48,21 +49,23 @@ describe('Uncovered files', function () { './network/branch_graph.js', ]; - const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/); - sourceFiles.keys().forEach(function (path) { - // ignore if there is a matching spec file - if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) { - return; - } + describe('Uncovered files', function () { + const sourceFiles = require.context('~', true, /\.js$/); + sourceFiles.keys().forEach(function (path) { + // ignore if there is a matching spec file + if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { + return; + } - it(`includes '${path}'`, function () { - try { - sourceFiles(path); - } catch (err) { - if (troubleMakers.indexOf(path) === -1) { - expect(err).toBeNull(); + it(`includes '${path}'`, function () { + try { + sourceFiles(path); + } catch (err) { + if (troubleMakers.indexOf(path) === -1) { + expect(err).toBeNull(); + } } - } + }); }); }); -}); +} diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js new file mode 100644 index 00000000000..bc8e504c413 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import asyncButtonComp from '~/vue_pipelines_index/components/async_button'; + +describe('Pipelines Async Button', () => { + let component; + let spy; + let AsyncButtonComponent; + + beforeEach(() => { + AsyncButtonComponent = Vue.extend(asyncButtonComp); + + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + service: { + postAction: spy, + }, + }, + }).$mount(); + }); + + it('should render a button', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + }); + + it('should render the provided icon', () => { + expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + }); + + it('should render the provided title', () => { + expect(component.$el.getAttribute('title')).toContain('Foo'); + expect(component.$el.getAttribute('aria-label')).toContain('Foo'); + }); + + it('should render the provided cssClass', () => { + expect(component.$el.getAttribute('class')).toContain('bar'); + }); + + it('should call the service when it is clicked with the provided endpoint', () => { + component.$el.click(); + expect(spy).toHaveBeenCalledWith('/foo'); + }); + + it('should hide loading if request fails', () => { + spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + dataAttributes: { + 'data-foo': 'foo', + }, + service: { + postAction: spy, + }, + }, + }).$mount(); + + component.$el.click(); + expect(component.$el.querySelector('.fa-spinner')).toBe(null); + }); + + describe('With confirm dialog', () => { + it('should call the service when confimation is positive', () => { + spyOn(window, 'confirm').and.returnValue(true); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + service: { + postAction: spy, + }, + confirmActionMessage: 'bar', + }, + }).$mount(); + + component.$el.click(); + expect(spy).toHaveBeenCalledWith('/foo'); + }); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js new file mode 100644 index 00000000000..96a2a37b5f7 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url'; + +describe('Pipeline Url Component', () => { + let PipelineUrlComponent; + + beforeEach(() => { + PipelineUrlComponent = Vue.extend(pipelineUrlComp); + }); + + it('should render a table cell', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.tagName).toEqual('TD'); + }); + + it('should render a link the provided path and id', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1'); + }); + + it('should render user information when a user is provided', () => { + const mockData = { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + user: { + web_url: '/', + name: 'foo', + avatar_url: '/', + }, + }, + }; + + const component = new PipelineUrlComponent({ + propsData: mockData, + }).$mount(); + + const image = component.$el.querySelector('.js-pipeline-url-user img'); + + expect( + component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), + ).toEqual(mockData.pipeline.user.web_url); + expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name); + expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); + }); + + it('should render "API" when no user is provided', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API'); + }); + + it('should render latest, yaml invalid and stuck flags when provided', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: { + latest: true, + yaml_errors: true, + stuck: true, + }, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest'); + expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); + expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js new file mode 100644 index 00000000000..dba998c7688 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions'; + +describe('Pipelines Actions dropdown', () => { + let component; + let spy; + let actions; + let ActionsComponent; + + beforeEach(() => { + ActionsComponent = Vue.extend(pipelinesActionsComp); + + actions = [ + { + name: 'stop_review', + path: '/root/review-app/builds/1893/play', + }, + ]; + + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new ActionsComponent({ + propsData: { + actions, + service: { + postAction: spy, + }, + }, + }).$mount(); + }); + + it('should render a dropdown with the provided actions', () => { + expect( + component.$el.querySelectorAll('.dropdown-menu li').length, + ).toEqual(actions.length); + }); + + it('should call the service when an action is clicked', () => { + component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); + component.$el.querySelector('.js-pipeline-action-link').click(); + + expect(spy).toHaveBeenCalledWith(actions[0].path); + }); + + it('should hide loading if request fails', () => { + spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); + + component = new ActionsComponent({ + propsData: { + actions, + service: { + postAction: spy, + }, + }, + }).$mount(); + + component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); + component.$el.querySelector('.js-pipeline-action-link').click(); + + expect(component.$el.querySelector('.fa-spinner')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js new file mode 100644 index 00000000000..f7f49649c1c --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts'; + +describe('Pipelines Artifacts dropdown', () => { + let component; + let artifacts; + + beforeEach(() => { + const ArtifactsComponent = Vue.extend(artifactsComp); + + artifacts = [ + { + name: 'artifact', + path: '/download/path', + }, + ]; + + component = new ArtifactsComponent({ + propsData: { + artifacts, + }, + }).$mount(); + }); + + it('should render a dropdown with the provided artifacts', () => { + expect( + component.$el.querySelectorAll('.dropdown-menu li').length, + ).toEqual(artifacts.length); + }); + + it('should render a link with the provided path', () => { + expect( + component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), + ).toEqual(artifacts[0].path); + + expect( + component.$el.querySelector('.dropdown-menu li a span').textContent, + ).toContain(artifacts[0].name); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js new file mode 100644 index 00000000000..5c0934404bb --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js @@ -0,0 +1,72 @@ +import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store'; + +describe('Pipelines Store', () => { + let store; + + beforeEach(() => { + store = new PipelineStore(); + }); + + it('should be initialized with an empty state', () => { + expect(store.state.pipelines).toEqual([]); + expect(store.state.count).toEqual({}); + expect(store.state.pageInfo).toEqual({}); + }); + + describe('storePipelines', () => { + it('should use the default parameter if none is provided', () => { + store.storePipelines(); + expect(store.state.pipelines).toEqual([]); + }); + + it('should store the provided array', () => { + const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }]; + store.storePipelines(array); + expect(store.state.pipelines).toEqual(array); + }); + }); + + describe('storeCount', () => { + it('should use the default parameter if none is provided', () => { + store.storeCount(); + expect(store.state.count).toEqual({}); + }); + + it('should store the provided count', () => { + const count = { all: 20, finished: 10 }; + store.storeCount(count); + + expect(store.state.count).toEqual(count); + }); + }); + + describe('storePagination', () => { + it('should use the default parameter if none is provided', () => { + store.storePagination(); + expect(store.state.pageInfo).toEqual({}); + }); + + it('should store pagination information normalized and parsed', () => { + const pagination = { + 'X-nExt-pAge': '2', + 'X-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '2', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }; + + const expectedResult = { + perPage: 1, + page: 1, + total: 37, + totalPages: 2, + nextPage: 2, + previousPage: 2, + }; + + store.storePagination(pagination); + expect(store.state.pageInfo).toEqual(expectedResult); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 15ab10b9b69..df547299d75 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -1,13 +1,17 @@ -require('~/vue_shared/components/commit'); +import Vue from 'vue'; +import commitComp from '~/vue_shared/components/commit'; describe('Commit component', () => { let props; let component; + let CommitComponent; + + beforeEach(() => { + CommitComponent = Vue.extend(commitComp); + }); it('should render a code-fork icon if it does not represent a tag', () => { - setFixtures('<div class="test-commit-container"></div>'); - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: { tag: false, commitRef: { @@ -23,15 +27,13 @@ describe('Commit component', () => { username: 'jschatz1', }, }, - }); + }).$mount(); expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork'); }); describe('Given all the props', () => { beforeEach(() => { - setFixtures('<div class="test-commit-container"></div>'); - props = { tag: true, commitRef: { @@ -49,10 +51,9 @@ describe('Commit component', () => { commitIconSvg: '<svg></svg>', }; - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: props, - }); + }).$mount(); }); it('should render a tag icon if it represents a tag', () => { @@ -105,7 +106,6 @@ describe('Commit component', () => { describe('When commit title is not provided', () => { it('should render default message', () => { - setFixtures('<div class="test-commit-container"></div>'); props = { tag: false, commitRef: { @@ -118,10 +118,9 @@ describe('Commit component', () => { author: {}, }; - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: props, - }); + }).$mount(); expect( component.$el.querySelector('.commit-title span').textContent, diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 412abfd5e41..699625cdbb7 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -1,20 +1,20 @@ -require('~/vue_shared/components/pipelines_table_row'); -const pipeline = require('../../commit/pipelines/mock_data'); +import Vue from 'vue'; +import tableRowComp from '~/vue_shared/components/pipelines_table_row'; +import pipeline from '../../commit/pipelines/mock_data'; describe('Pipelines Table Row', () => { let component; - preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + const PipelinesTableRowComponent = Vue.extend(tableRowComp); - component = new gl.pipelines.PipelinesTableRowComponent({ + component = new PipelinesTableRowComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipeline, - svgs: {}, + service: {}, }, - }); + }).$mount(); }); it('should render a table row', () => { diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js index 54d81e2ea7d..b0b1df5a753 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js @@ -1,24 +1,24 @@ -require('~/vue_shared/components/pipelines_table'); -require('~/lib/utils/datetime_utility'); -const pipeline = require('../../commit/pipelines/mock_data'); +import Vue from 'vue'; +import pipelinesTableComp from '~/vue_shared/components/pipelines_table'; +import '~/lib/utils/datetime_utility'; +import pipeline from '../../commit/pipelines/mock_data'; describe('Pipelines Table', () => { - preloadFixtures('static/environments/element.html.raw'); + let PipelinesTableComponent; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + PipelinesTableComponent = Vue.extend(pipelinesTableComp); }); describe('table', () => { let component; beforeEach(() => { - component = new gl.pipelines.PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), + component = new PipelinesTableComponent({ propsData: { pipelines: [], - svgs: {}, + service: {}, }, - }); + }).$mount(); }); it('should render a table', () => { @@ -37,26 +37,25 @@ describe('Pipelines Table', () => { describe('without data', () => { it('should render an empty table', () => { - const component = new gl.pipelines.PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), + const component = new PipelinesTableComponent({ propsData: { pipelines: [], - svgs: {}, + service: {}, }, - }); + }).$mount(); expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); }); }); describe('with data', () => { it('should render rows', () => { - const component = new gl.pipelines.PipelinesTableComponent({ + const component = new PipelinesTableComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], - svgs: {}, + service: {}, }, - }); + }).$mount(); expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); }); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 9cb067921a7..a5c3870b3ac 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -1,8 +1,10 @@ -require('~/lib/utils/common_utils'); -require('~/vue_shared/components/table_pagination'); +import Vue from 'vue'; +import paginationComp from '~/vue_shared/components/table_pagination'; +import '~/lib/utils/common_utils'; describe('Pagination component', () => { let component; + let PaginationComponent; const changeChanges = { one: '', @@ -12,11 +14,12 @@ describe('Pagination component', () => { changeChanges.one = one; }; - it('should render and start at page 1', () => { - setFixtures('<div class="test-pagination-container"></div>'); + beforeEach(() => { + PaginationComponent = Vue.extend(paginationComp); + }); - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + it('should render and start at page 1', () => { + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -25,7 +28,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); expect(component.$el.classList).toContain('gl-pagination'); @@ -35,10 +38,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -47,7 +47,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: 'Prev' } }); @@ -55,10 +55,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -67,7 +64,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: 'Next' } }); @@ -75,10 +72,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -87,7 +81,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: 'Last >>' } }); @@ -95,10 +89,7 @@ describe('Pagination component', () => { }); it('should go to the first page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -107,7 +98,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: '<< First' } }); @@ -115,10 +106,7 @@ describe('Pagination component', () => { }); it('should do nothing', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -127,7 +115,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: '...' } }); 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/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index dc57e94f193..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } @@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey } end 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/database_spec.rb b/spec/lib/gitlab/database_spec.rb index edd01d032c8..4ce4e6e1034 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' -class MigrationTest - include Gitlab::Database -end - describe Gitlab::Database, lib: true do + before do + stub_const('MigrationTest', Class.new { include Gitlab::Database }) + end + + describe '.config' do + it 'returns a Hash' do + expect(described_class.config).to be_an_instance_of(Hash) + end + end + describe '.adapter_name' do it 'returns the name of the adapter' do expect(described_class.adapter_name).to be_an_instance_of(String) 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/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4c55532d165..992126ef153 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -109,6 +109,43 @@ EOT end end end + + context 'using a Gitaly::CommitDiffResponse' do + let(:diff) do + described_class.new( + Gitaly::CommitDiffResponse.new( + to_path: ".gitmodules", + from_path: ".gitmodules", + old_mode: 0100644, + new_mode: 0100644, + from_id: '357406f3075a57708d0163752905cc1576fceacc', + to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', + raw_chunks: raw_chunks, + ) + ) + end + + context 'with a small diff' do + let(:raw_chunks) { [@raw_diff_hash[:diff]] } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + let(:raw_chunks) { ['a' * 204800] } + + it 'prunes the diff' do + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + end end describe 'straight diffs' do 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/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb new file mode 100644 index 00000000000..4684b1d1ac0 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Commit do + describe '.diff_from_parent' do + let(:diff_stub) { double('Gitaly::Diff::Stub') } + let(:project) { create(:project, :repository) } + let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) } + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + + before do + allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub) + allow(diff_stub).to receive(:commit_diff).and_return([]) + end + + context 'when a commit has a parent' do + it 'sends an RPC request with the parent ID as left commit' do + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + right_commit_id: commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(commit) + end + end + + context 'when a commit does not have a parent' do + it 'sends an RPC request with empty tree ref as left commit' do + initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + right_commit_id: initial_commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(initial_commit) + end + end + + it 'returns a Gitlab::Git::DiffCollection' do + ret = described_class.diff_from_parent(commit) + + expect(ret).to be_kind_of(Gitlab::Git::DiffCollection) + end + + it 'passes options to Gitlab::Git::DiffCollection' do + options = { max_files: 31, max_lines: 13 } + + expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options) + + described_class.diff_from_parent(commit, options) + 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/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e47956a365f..ddeb71730e7 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -130,7 +130,6 @@ project: - campfire_service - drone_ci_service - emails_on_push_service -- builds_email_service - pipelines_email_service - mattermost_slash_commands_service - slack_slash_commands_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index c3d5c451a3c..d9b67426818 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6507,7 +6507,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6565,7 +6564,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6603,7 +6601,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" }, @@ -6624,7 +6621,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6659,7 +6655,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" }, @@ -6695,7 +6690,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6716,7 +6710,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6751,7 +6744,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" }, @@ -6787,7 +6779,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" }, @@ -6808,7 +6799,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6843,7 +6833,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" }, @@ -6879,7 +6868,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" }, @@ -6900,7 +6888,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6935,7 +6922,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6971,7 +6957,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" }, @@ -6985,11 +6970,10 @@ { "id": 123, "token": "cdbfasdf44a5958c83654733449e585", - "project_id": null, + "project_id": 5, "deleted_at": null, "created_at": "2017-01-16T15:25:28.637Z", - "updated_at": "2017-01-16T15:25:28.637Z", - "gl_project_id": 123 + "updated_at": "2017-01-16T15:25:28.637Z" } ], "deploy_keys": [ @@ -7047,7 +7031,7 @@ "updated_at": "2016-06-14T15:01:51.303Z", "active": false, "properties": { - "notify_only_broken_builds": true + "notify_only_broken_pipelines": true }, "template": false, "push_events": true, @@ -7055,7 +7039,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "pipeline_events": true, "category": "common", "default": false, "wiki_page_events": true @@ -7174,7 +7158,7 @@ "updated_at": "2016-06-14T15:01:51.219Z", "active": false, "properties": { - "notify_only_broken_builds": true + "notify_only_broken_pipelines": true }, "template": false, "push_events": true, @@ -7182,7 +7166,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "pipeline_events": true, "category": "common", "default": false, "wiki_page_events": true @@ -7335,27 +7319,6 @@ "wiki_page_events": true }, { - "id": 85, - "title": "Builds emails", - "project_id": 5, - "created_at": "2016-06-14T15:01:51.090Z", - "updated_at": "2016-06-14T15:01:51.090Z", - "active": false, - "properties": { - "notify_only_broken_builds": true - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "build_events": true, - "category": "common", - "default": false, - "wiki_page_events": true - }, - { "id": 84, "title": "Buildkite", "project_id": 5, @@ -7503,4 +7466,4 @@ "updated_at": "2016-09-23T11:58:28.000Z", "wiki_access_level": 20 } -}
\ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index f4a21c24fa1..c36f12dbd82 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Ci::Build.where(token: 'abcd')).to be_empty end end + + context 'has restored the correct number of records' do + it 'has the correct number of merge requests' do + expect(@project.merge_requests.size).to eq(9) + end + + it 'has the correct number of triggers' do + expect(@project.triggers.size).to eq(1) + end + + it 'has the correct number of pipelines and statuses' do + expect(@project.pipelines.size).to eq(5) + + @project.pipelines.zip([2, 2, 2, 2, 2]) + .each do |(pipeline, expected_status_size)| + expect(pipeline.statuses.size).to eq(expected_status_size) + end + end + end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index c718e792461..1ad16a9b57d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -15,6 +15,7 @@ Issue: - updated_by_id - confidential - deleted_at +- closed_at - due_date - moved_to_id - lock_version @@ -176,7 +177,6 @@ Ci::Pipeline: - tag - yaml_errors - committed_at -- gl_project_id - status - started_at - finished_at @@ -211,7 +211,6 @@ CommitStatus: - target_url - description - artifacts_file -- gl_project_id - artifacts_metadata - erased_by_id - erased_at @@ -232,7 +231,6 @@ Ci::Variable: - encrypted_value - encrypted_value_salt - encrypted_value_iv -- gl_project_id Ci::Trigger: - id - token @@ -240,7 +238,6 @@ Ci::Trigger: - deleted_at - created_at - updated_at -- gl_project_id - owner_id - description DeployKey: 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/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb new file mode 100644 index 00000000000..a504d299307 --- /dev/null +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::UrlBlocker, lib: true do + describe '#blocked_url?' do + it 'allows imports from configured web host and port' do + import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git" + expect(described_class.blocked_url?(import_url)).to be false + end + + it 'allows imports from configured SSH host and port' do + import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" + expect(described_class.blocked_url?(import_url)).to be false + end + + it 'returns true for bad localhost hostname' do + expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true + end + + it 'returns true for bad port' do + expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true + end + + it 'returns true for invalid URL' do + expect(described_class.blocked_url?('http://:8080')).to be true + end + + it 'returns false for legitimate URL' do + expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false + end + end +end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 3fd361de458..fc144a2556a 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do let(:url_sanitizer) do described_class.new("https://github.com/me/project.git", credentials: credentials) end + let(:user) { double(:user, username: 'john.doe') } describe '.sanitize' do def sanitize_url(url) @@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do end end + describe '.valid?' do + it 'validates url strings' do + expect(described_class.valid?(nil)).to be(false) + expect(described_class.valid?('valid@project:url.git')).to be(true) + expect(described_class.valid?('123://invalid:url')).to be(false) + end + end + + describe '.http_credentials_for_user' do + it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) } + it { expect(described_class.http_credentials_for_user('foo')).to eq({}) } + end + describe '#sanitized_url' do it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } end describe '#credentials' do it { expect(url_sanitizer.credentials).to eq(credentials) } + + context 'when user is given to #initialize' do + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user)) + end + + it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) } + end end describe '#full_url' do @@ -69,13 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do expect(sanitizer.full_url).to eq('user@server:project.git') end - end - describe '.valid?' do - it 'validates url strings' do - expect(described_class.valid?(nil)).to be(false) - expect(described_class.valid?('valid@project:url.git')).to be(true) - expect(described_class.valid?('123://invalid:url')).to be(false) + context 'when user is given to #initialize' do + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user)) + end + + it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") } end end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb new file mode 100644 index 00000000000..3255c6f1ef7 --- /dev/null +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::VisibilityLevel, lib: true do + describe '.level_value' do + it 'converts "public" to integer value' do + expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'converts string integer to integer value' do + expect(described_class.level_value('20')).to eq(20) + end + + it 'defaults to PRIVATE when string value is not valid' do + expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'defaults to PRIVATE when integer value is not valid' do + expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end +end diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb deleted file mode 100644 index d968096783c..00000000000 --- a/spec/mailers/emails/builds_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' -require 'email_spec' - -describe Notify do - include EmailSpec::Matchers - - include_context 'gitlab email notification' - - describe 'build notification email' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - shared_examples 'build email' do - it 'contains name of project' do - is_expected.to have_body_text build.project_name - end - - it 'contains link to project' do - is_expected.to have_body_text namespace_project_path(project.namespace, project) - end - end - - shared_examples 'an email with X-GitLab headers containing build details' do - it 'has X-GitLab-Build* headers' do - is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ - is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ - end - end - - describe 'build success' do - subject { Notify.build_success_email(build.id, 'wow@example.com') } - before { build.success } - - it_behaves_like 'build email' - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'success' - end - - it 'has the correct subject' do - is_expected.to have_subject /Build success for/ - end - end - - describe 'build fail' do - subject { Notify.build_fail_email(build.id, 'wow@example.com') } - before { build.drop } - - it_behaves_like 'build email' - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'failed' - end - - it 'has the correct subject' do - is_expected.to have_subject /Build failed for/ - end - end - end -end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e822d7eb348..6ee91576676 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -63,7 +63,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_body_text issue.author_name + is_expected.to have_html_escaped_body_text issue.author_name is_expected.to have_body_text 'wrote:' end end @@ -75,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_html_escaped_body_text issue_with_description.description end end @@ -100,11 +100,11 @@ describe Notify do end it 'contains the name of the previous assignee' do - is_expected.to have_body_text previous_assignee.name + is_expected.to have_html_escaped_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_html_escaped_body_text assignee.name end it 'contains a link to the issue' do @@ -167,7 +167,7 @@ describe Notify do end it 'contains the user name' do - is_expected.to have_body_text current_user.name + is_expected.to have_html_escaped_body_text current_user.name end it 'contains a link to the issue' do @@ -242,7 +242,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_body_text merge_request.author_name + is_expected.to have_html_escaped_body_text merge_request.author_name is_expected.to have_body_text 'wrote:' end end @@ -255,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_html_escaped_body_text merge_request_with_description.description end end @@ -280,11 +280,11 @@ describe Notify do end it 'contains the name of the previous assignee' do - is_expected.to have_body_text previous_assignee.name + is_expected.to have_html_escaped_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_html_escaped_body_text assignee.name end it 'contains a link to the merge request' do @@ -347,7 +347,7 @@ describe Notify do end it 'contains the user name' do - is_expected.to have_body_text current_user.name + is_expected.to have_html_escaped_body_text current_user.name end it 'contains a link to the merge request' do @@ -400,7 +400,7 @@ describe Notify do end it 'contains name of project' do - is_expected.to have_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.name_with_namespace end it 'contains new user role' do @@ -433,7 +433,7 @@ 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_html_escaped_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 @@ -460,7 +460,7 @@ 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_html_escaped_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 @@ -482,13 +482,14 @@ 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_html_escaped_body_text project.name_with_namespace is_expected.to have_body_text project.web_url end end describe 'project access changed' do - let(:project) { create(:empty_project, :public, :access_requestable) } + let(:owner) { create(:user, name: "Chang O'Keefe") } + let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) } let(:user) { create(:user) } let(:project_member) { create(:project_member, project: project, user: user) } subject { Notify.member_access_granted_email('project', project_member.id) } @@ -499,7 +500,7 @@ 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_html_escaped_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 @@ -530,7 +531,7 @@ 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_html_escaped_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 @@ -555,10 +556,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_html_escaped_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_html_escaped_body_text invited_user.name end end @@ -579,7 +580,7 @@ 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_html_escaped_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 @@ -607,7 +608,7 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text note.note + is_expected.to have_html_escaped_body_text note.note end it 'does not contain note author' do @@ -620,7 +621,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_body_text note.author_name + is_expected.to have_html_escaped_body_text note.author_name is_expected.to have_body_text 'wrote:' end end @@ -727,7 +728,7 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text note.note + is_expected.to have_html_escaped_body_text note.note end it 'does not contain note author' do @@ -740,7 +741,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_body_text note.author_name + is_expected.to have_html_escaped_body_text note.author_name is_expected.to have_body_text 'wrote:' end end @@ -786,7 +787,7 @@ 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_html_escaped_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 @@ -807,7 +808,7 @@ 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_html_escaped_body_text group.name is_expected.to have_body_text group.web_url end end @@ -825,7 +826,7 @@ 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_html_escaped_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.human_access end @@ -856,7 +857,7 @@ 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_html_escaped_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 @@ -881,10 +882,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_html_escaped_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_html_escaped_body_text invited_user.name end end @@ -905,7 +906,7 @@ 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_html_escaped_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.invite_email end diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb new file mode 100644 index 00000000000..57eb03e3c80 --- /dev/null +++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.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 MigrateBuildEventsToPipelineEvents, truncate: true do + let(:migration) { described_class.new } + let(:project_with_pipeline_service) { create(:empty_project) } + let(:project_with_build_service) { create(:empty_project) } + + before do + ActiveRecord::Base.connection.execute <<-SQL + INSERT INTO services (properties, build_events, pipeline_events, type) + VALUES + ('{"notify_only_broken_builds":true}', true, false, 'SlackService') + , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService') + , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService') + ; + SQL + + ActiveRecord::Base.connection.execute <<-SQL + INSERT INTO services + (properties, build_events, pipeline_events, type, project_id) + VALUES + ('{"notify_only_broken_builds":true}', true, false, + 'BuildsEmailService', #{project_with_pipeline_service.id}) + , ('{"notify_only_broken_pipelines":true}', false, true, + 'PipelinesEmailService', #{project_with_pipeline_service.id}) + , ('{"notify_only_broken_builds":true}', true, false, + 'BuildsEmailService', #{project_with_build_service.id}) + ; + SQL + end + + describe '#up' do + before do + silence_migration = Module.new do + # rubocop:disable Rails/Delegate + def execute(query) + connection.execute(query) + end + end + + migration.extend(silence_migration) + migration.up + end + + it 'migrates chat service properly' do + [SlackService, MattermostService, HipchatService].each do |service| + expect(service.count).to eq(1) + + verify_service_record(service.first) + end + end + + it 'migrates pipelines email service only if it has none before' do + Project.find_each do |project| + pipeline_service_count = + project.services.where(type: 'PipelinesEmailService').count + + expect(pipeline_service_count).to eq(1) + + verify_service_record(project.pipelines_email_service) + end + end + + def verify_service_record(service) + expect(service.notify_only_broken_pipelines).to be(true) + expect(service.build_events).to be(false) + expect(service.pipeline_events).to be(true) + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index fd6ea2d6722..8dbcf50ee0c 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -795,8 +795,8 @@ describe Ci::Build, :models do describe '#merge_request' do def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: pipeline.gl_project_id, - target_project_id: pipeline.gl_project_id, + create(factory, source_project: pipeline.project, + target_project: pipeline.project, source_branch: build.ref, created_at: created_at) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 9962c987110..53282b999dc 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -532,6 +532,19 @@ describe Ci::Pipeline, models: true do end end + describe '.latest_successful_for_refs' do + include_context 'with some outdated pipelines' + + let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') } + let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') } + + it 'returns the latest successful pipeline for both refs' do + refs = %w(ref1 ref2 ref3) + + expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 }) + end + end + describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') @@ -1018,6 +1031,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/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index bee9f714849..048d25869bc 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -6,7 +6,7 @@ describe Ci::Variable, models: true do let(:secret_value) { 'secret' } it { is_expected.to validate_presence_of(:key) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } it { is_expected.to validate_length_of(:key).is_at_most(255) } it { is_expected.to allow_value('foo').for(:key) } it { is_expected.not_to allow_value('foo bar').for(:key) } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 32f9366a14c..980a1b70ef5 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 @@ -369,4 +388,32 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end + + describe '#raw_diffs' do + context 'Gitaly commit_raw_diffs feature enabled' do + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true) + end + + context 'when a truthy deltas_only is not passed to args' do + it 'fetches diffs from Gitaly server' do + expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent). + with(commit) + + commit.raw_diffs + end + end + + context 'when a truthy deltas_only is passed to args' do + it 'fetches diffs using Rugged' do + opts = { deltas_only: true } + + expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent) + expect(commit.raw).to receive(:diffs).with(opts) + + commit.raw_diffs(opts) + end + end + end + end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index f134da441c2..82abad0e2f6 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -110,6 +110,24 @@ describe HasStatus do it { is_expected.to eq 'running' } end + context 'when one status finished and second is still created' do + let!(:statuses) do + [create(type, status: :success), create(type, status: :created)] + end + + it { is_expected.to eq 'running' } + end + + context 'when there is a manual status before created status' do + let!(:statuses) do + [create(type, status: :success), + create(type, status: :manual, allow_failure: false), + create(type, status: :created)] + end + + it { is_expected.to eq 'manual' } + end + context 'when one status is a blocking manual action' do let!(:statuses) do [create(type, status: :failed), diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 545a11912e3..9574796a945 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -278,6 +278,16 @@ describe Issue, "Issuable" do end end + context 'issue has labels' do + let(:labels) { [create(:label), create(:label)] } + + before { issue.update_attribute(:labels, labels)} + + it 'includes labels in the hook data' do + expect(data[:labels]).to eq(labels.map(&:hook_attrs)) + end + end + include_examples 'project hook data' include_examples 'deprecated repository hook data' end @@ -344,6 +354,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/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/issue_spec.rb b/spec/models/issue_spec.rb index f67fbe79bde..73977d031f9 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -37,6 +37,30 @@ describe Issue, models: true do end end + describe '#closed_at' do + after do + Timecop.return + end + + let!(:now) { Timecop.freeze(Time.now) } + + it 'sets closed_at to Time.now when issue is closed' do + issue = create(:issue, state: 'opened') + + issue.close + + expect(issue.closed_at).to eq(now) + end + + it 'sets closed_at to nil when issue is reopened' do + issue = create(:issue, state: 'closed') + + issue.reopen + + expect(issue.closed_at).to be_nil + end + end + describe '#to_reference' do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } @@ -635,4 +659,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_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb deleted file mode 100644 index 0194f9e2563..00000000000 --- a/spec/models/project_services/builds_email_service_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' - -describe BuildsEmailService do - let(:data) do - Gitlab::DataBuilder::Build.build(create(:ci_build)) - end - - describe 'Validations' do - context 'when service is active' do - before { subject.active = true } - - it { is_expected.to validate_presence_of(:recipients) } - - context 'when pusher is added' do - before { subject.add_pusher = true } - - it { is_expected.not_to validate_presence_of(:recipients) } - end - end - - context 'when service is inactive' do - before { subject.active = false } - - it { is_expected.not_to validate_presence_of(:recipients) } - end - end - - describe '#test_data' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - let(:user) { create(:user) } - - before { project.team << [user, :developer] } - - it 'builds test data' do - data = subject.test_data(project) - - expect(data[:object_kind]).to eq("build") - end - end - - describe '#test' do - it 'sends email' do - data = Gitlab::DataBuilder::Build.build(create(:ci_build)) - subject.recipients = 'test@gitlab.com' - - expect(BuildEmailWorker).to receive(:perform_async) - - subject.test(data) - end - - context 'notify only failed builds is true' do - it 'sends email' do - data = Gitlab::DataBuilder::Build.build(create(:ci_build)) - data[:build_status] = "success" - subject.recipients = 'test@gitlab.com' - - expect(subject).not_to receive(:notify_only_broken_builds) - expect(BuildEmailWorker).to receive(:perform_async) - - subject.test(data) - end - end - end - - describe '#execute' do - it 'sends email' do - subject.recipients = 'test@gitlab.com' - data[:build_status] = 'failed' - - expect(BuildEmailWorker).to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with succeeded build and notify_only_broken_builds on' do - expect(subject).to receive(:notify_only_broken_builds).and_return(true) - data[:build_status] = 'success' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with failed build and build_allow_failure is true' do - data[:build_status] = 'failed' - data[:build_allow_failure] = true - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with unknown build status' do - data[:build_status] = 'foo' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email when recipients list is empty' do - subject.recipients = ' ,, ' - data[:build_status] = 'failed' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - end -end diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb deleted file mode 100644 index 3bd7ec18ae0..00000000000 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe ChatMessage::BuildMessage do - subject { described_class.new(args) } - - let(:args) do - { - sha: '97de212e80737a608d939f648d959671fb0a0142', - ref: 'develop', - tag: false, - - project_name: 'project_name', - project_url: 'http://example.gitlab.com', - build_id: 1, - build_name: build_name, - build_stage: stage, - - commit: { - status: status, - author_name: 'hacker', - author_url: 'http://example.gitlab.com/hacker', - duration: duration, - }, - } - end - - let(:message) { build_message } - let(:stage) { 'test' } - let(:status) { 'success' } - let(:build_name) { 'rspec' } - let(:duration) { 10 } - - context 'build succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - context 'build failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - - it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - it 'returns a message with information on build' do - expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>") - end - - it 'returns a message with stage name' do - expect(subject.fallback).to include("of stage #{stage}") - end - - it 'returns a message with link to author' do - expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>") - end - - def build_message(status_text = status, stage_text = stage, build_text = build_name) - "<http://example.gitlab.com|project_name>:" \ - " Commit <http://example.gitlab.com/commit/" \ - "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ - " of <http://example.gitlab.com/commits/develop|develop> branch" \ - " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \ - " on build <http://example.gitlab.com/builds/1|#{build_text}>" \ - " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index bf422ac7ce1..1200ae7eb22 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -280,13 +280,14 @@ describe HipchatService, models: true do end end - context 'build events' do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:data) { Gitlab::DataBuilder::Build.build(build.reload) } + context 'pipeline events' do + let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) } + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'for failed' do - before { build.drop } + before do + pipeline.drop + end it "calls Hipchat API" do hipchat.execute(data) @@ -295,35 +296,36 @@ describe HipchatService, models: true do end it "creates a build message" do - message = hipchat.send(:create_build_message, data) + message = hipchat.__send__(:create_pipeline_message, data) project_url = project.web_url project_name = project.name_with_namespace.gsub(/\s/, '') - sha = data[:sha] - ref = data[:ref] - ref_type = data[:tag] ? 'tag' : 'branch' - duration = data[:commit][:duration] + pipeline_attributes = data[:object_attributes] + ref = pipeline_attributes[:ref] + ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + duration = pipeline_attributes[:duration] + user_name = data[:user][:name] expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \ - "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \ + "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \ "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \ - "by #{data[:commit][:author_name]} failed in #{duration} second(s)") + "by #{user_name} failed in #{duration} second(s)") end end context 'for succeeded' do before do - build.success + pipeline.succeed end it "calls Hipchat API" do - hipchat.notify_only_broken_builds = false + hipchat.notify_only_broken_pipelines = false hipchat.execute(data) expect(WebMock).to have_requested(:post, api_url).once end it "notifies only broken" do - hipchat.notify_only_broken_builds = true + hipchat.notify_only_broken_pipelines = true hipchat.execute(data) expect(WebMock).not_to have_requested(:post, api_url).once end @@ -349,17 +351,19 @@ describe HipchatService, models: true do context 'with a successful build' do it 'uses the green color' do - build_data = { object_kind: 'build', commit: { status: 'success' } } + data = { object_kind: 'pipeline', + object_attributes: { status: 'success' } } - expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' }) + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' }) end end context 'with a failed build' do it 'uses the red color' do - build_data = { object_kind: 'build', commit: { status: 'failed' } } + data = { object_kind: 'pipeline', + object_attributes: { status: 'failed' } } - expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' }) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e120e21af06..5e5f690acd4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,8 +29,7 @@ describe Project, models: true do it { is_expected.to have_one(:campfire_service).dependent(:destroy) } it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) } it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } - it { is_expected.to have_one(:builds_email_service).dependent(:destroy) } - it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } + it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) } it { is_expected.to have_one(:irker_service).dependent(:destroy) } it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) } it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } @@ -219,6 +218,20 @@ describe Project, models: true do expect(project2.import_data).to be_nil end + it "does not allow blocked import_url localhost" do + project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git') + + expect(project2).to be_invalid + expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + end + + it "does not allow blocked import_url port" do + project2 = build(:empty_project, import_url: 'http://github.com:25/t.git') + + expect(project2).to be_invalid + expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + end + describe 'project pending deletion' do let!(:project_pending_deletion) do create(:empty_project, @@ -1901,10 +1914,8 @@ describe Project, models: true do context 'when no user is given' do it 'returns the url to the repo without a username' do - url = project.http_url_to_repo - - expect(url).to eq(project.http_url_to_repo) - expect(url).not_to include('@') + expect(project.http_url_to_repo).to eq("#{project.web_url}.git") + expect(project.http_url_to_repo).not_to include('@') end end @@ -1912,8 +1923,19 @@ describe Project, models: true do it 'returns the url to the repo with the username' do user = build_stubbed(:user) - expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@}) + expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@") 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/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 58b57bd4fef..b5b9cd024b0 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -35,10 +35,23 @@ describe ProjectWiki, models: true do end describe "#http_url_to_repo" do - it "provides the full http url to the repo" do - gitlab_url = Gitlab.config.gitlab.url - repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git" - expect(subject.http_url_to_repo).to eq(repo_http_url) + let(:project) { create :empty_project } + + context 'when no user is given' do + it 'returns the url to the repo without a username' do + expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git" + + expect(project_wiki.http_url_to_repo).to eq(expected_url) + expect(project_wiki.http_url_to_repo).not_to include('@') + end + end + + context 'when user is given' do + it 'returns the url to the repo with the username' do + user = build_stubbed(:user) + + expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@") + end end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 0b222022e62..bc8ae4ae5a8 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -43,14 +43,22 @@ describe Route, models: true do end context 'name update' do - before { route.update_attributes(name: 'bar') } - it "updates children routes with new path" do + route.update_attributes(name: 'bar') + expect(described_class.exists?(name: 'bar')).to be_truthy expect(described_class.exists?(name: 'bar / test')).to be_truthy expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy expect(described_class.exists?(name: 'gitlab-org')).to be_truthy end + + it 'handles a rename from nil' do + # Note: using `update_columns` to skip all validation and callbacks + route.update_columns(name: nil) + + expect { route.update_attributes(name: 'bar') } + .to change { route.name }.from(nil).to('bar') + end end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9da4140f3ce..90378179e32 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -699,7 +699,9 @@ describe User, models: true do 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) } + let!(:email) do + create(:email, user: another_user, email: 'alias@example.com') + end it 'returns users with a matching name' do expect(search_with_secondary_emails(user.name)).to eq([user]) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index ab5a7e4d3de..a70f7beaae0 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -5,77 +5,146 @@ describe API::Branches, api: true do include ApiHelpers let(:user) { create(:user) } - let(:user2) { create(:user) } let!(:project) { create(:project, :repository, creator: user) } let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } + let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] } describe "GET /projects/:id/repository/branches" do - it "returns an array of project branches" do - project.repository.expire_all_method_caches + let(:route) { "/projects/#{project.id}/repository/branches" } - get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + shared_examples_for 'repository branches' do + it 'returns the repository branches' do + get api(route, current_user), per_page: 100 - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - end - describe "GET /projects/:id/repository/branches/:branch" do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branches' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end - expect(json_response['name']).to eq(branch_name) - json_commit = json_response['commit'] - expect(json_commit['id']).to eq(branch_sha) - expect(json_commit).to have_key('short_id') - expect(json_commit).to have_key('title') - expect(json_commit).to have_key('message') - expect(json_commit).to have_key('author_name') - expect(json_commit).to have_key('author_email') - expect(json_commit).to have_key('authored_date') - expect(json_commit).to have_key('committer_name') - expect(json_commit).to have_key('committer_email') - expect(json_commit).to have_key('committed_date') - expect(json_commit).to have_key('parent_ids') - expect(json_response['merged']).to eq(false) - expect(json_response['protected']).to eq(false) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "returns the branch information for a single branch with dots in the name" do - get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository branches' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end + end + + describe "GET /projects/:id/repository/branches/:branch" do + let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" } - context 'on a merged branch' do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/merge-test", user) + shared_examples_for 'repository branch' do |merged: false| + it 'returns the repository branch' do + get api(route, current_user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq('merge-test') - expect(json_response['merged']).to eq(true) + expect(json_response['name']).to eq(branch_name) + expect(json_response['merged']).to eq(merged) + expect(json_response['protected']).to eq(false) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + + json_commit = json_response['commit'] + expect(json_commit['id']).to eq(branch_sha) + expect(json_commit).to have_key('short_id') + expect(json_commit).to have_key('title') + expect(json_commit).to have_key('message') + expect(json_commit).to have_key('author_name') + expect(json_commit).to have_key('author_email') + expect(json_commit).to have_key('authored_date') + expect(json_commit).to have_key('committer_name') + expect(json_commit).to have_key('committer_email') + expect(json_commit).to have_key('committed_date') + expect(json_commit).to have_key('parent_ids') + end + + context 'when branch does not exist' do + let(:branch_name) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Branch Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end end end - it "returns a 403 error if guest" do - get api("/projects/#{project.id}/repository/branches", user2) - expect(response).to have_http_status(403) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branch' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - it "returns a 404 error if branch is not available" do - get api("/projects/#{project.id}/repository/branches/unknown", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + let(:current_user) { user } + it_behaves_like 'repository branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + let(:branch_sha) { project.commit('master').sha } + + it_behaves_like 'repository branch' + end + + context 'when branch is merged' do + let(:branch_name) { 'merge-test' } + let(:branch_sha) { project.commit('merge-test').sha } + + it_behaves_like 'repository branch', merged: true + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end @@ -93,10 +162,10 @@ describe API::Branches, api: true do end it "protects a single branch with dots in the name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(true) end @@ -234,7 +303,7 @@ describe API::Branches, api: true do end it "returns a 403 error if guest" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest) expect(response).to have_http_status(403) end end @@ -250,10 +319,10 @@ describe API::Branches, api: true do end it "update branches with dots in branch name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(false) end @@ -282,7 +351,7 @@ describe API::Branches, api: true do end it "denies for user without push access" do - post api("/projects/#{project.id}/repository/branches", user2), + post api("/projects/#{project.id}/repository/branches", guest), branch: branch_name, ref: branch_sha expect(response).to have_http_status(403) @@ -330,7 +399,7 @@ describe API::Branches, api: true do end it "removes a branch with dots in the branch name" do - delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) expect(response).to have_http_status(204) end @@ -367,7 +436,7 @@ describe API::Branches, api: true do end it 'returns a 403 error if guest' do - delete api("/projects/#{project.id}/repository/merged_branches", user2) + delete api("/projects/#{project.id}/repository/merged_branches", guest) expect(response).to have_http_status(403) 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/internal_spec.rb b/spec/requests/api/internal_spec.rb index f18b8e98707..63ec00cdf04 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -397,16 +397,25 @@ describe API::Internal, api: true do before do project.team << [user, :developer] - get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token end it 'returns link to create new merge request' do + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + expect(json_response).to match [{ "branch_name" => "new_branch", "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", "new_merge_request" => true }] end + + it 'returns empty array if printing_merge_request_link_enabled is false' do + project.update!(printing_merge_request_link_enabled: false) + + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + + expect(json_response).to eq([]) + end end describe 'POST /notify_post_receive' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index de7dbca0b22..52f68fed2cc 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) @@ -524,6 +534,12 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index d50fe80b36a..044b989e5ba 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -152,6 +152,34 @@ describe API::Runner do end end end + + describe 'POST /api/v4/runners/verify' do + let(:runner) { create(:ci_runner) } + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners/verify') + + expect(response).to have_http_status :bad_request + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners/verify'), token: 'invalid-token' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'verifies Runner credentials' do + post api('/runners/verify'), token: runner.token + + expect(response).to have_http_status 200 + end + end + end end describe '/api/v4/jobs' do @@ -220,18 +248,6 @@ describe API::Runner do it { expect(response).to have_http_status(204) } end end - - context "when runner doesn't send version in User-Agent" do - let(:user_agent) { 'Go-http-client/1.1' } - - it { expect(response).to have_http_status(404) } - end - - context "when runner doesn't have a User-Agent" do - let(:user_agent) { nil } - - it { expect(response).to have_http_status(404) } - end end context 'when no token is provided' do @@ -254,10 +270,10 @@ describe API::Runner do context 'when Runner is not active' do let(:runner) { create(:ci_runner, :inactive) } - it 'returns 404 error' do + it 'returns 204 error' do request_job - expect(response).to have_http_status 404 + expect(response).to have_http_status 204 end end @@ -312,8 +328,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 @@ -401,9 +417,39 @@ describe API::Runner do end context 'when project and pipeline have multiple jobs' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - before { job.success } + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(2) + expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when explicit dependencies are defined' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + stage: 'deploy', stage_idx: 1, + options: { dependencies: [job2.name] }) + end + + before do + job.success + job2.success + end it 'returns dependent jobs' do request_job @@ -411,7 +457,7 @@ describe API::Runner do expect(response).to have_http_status(201) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(1) - expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach') + expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) end end @@ -437,9 +483,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/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 424c02932ab..d93a734f5b6 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -59,14 +59,6 @@ describe API::Triggers do expect(pipeline.builds.size).to eq(5) end - it 'creates builds on webhook from other gitlab repository and branch' do - expect do - post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - - expect(response).to have_http_status(201) - end - it 'returns bad request with no pipeline created if there\'s no commit for that ref' do post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') @@ -101,6 +93,28 @@ describe API::Triggers do end end end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + + expect(response).to have_http_status(201) + end + end + end end describe 'GET /projects/:id/triggers' do diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb index e4cedf98e64..5dcd4f21f4e 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -10,6 +10,7 @@ describe API::V3::Branches, api: true do let!(:master) { create(:project_member, :master, user: user, project: project) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } + let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } describe "GET /projects/:id/repository/branches" do @@ -80,4 +81,55 @@ describe API::V3::Branches, api: true do expect(response).to have_http_status(403) end end + + describe "POST /projects/:id/repository/branches" do + it "creates a new branch" do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'feature1', + ref: branch_sha + + expect(response).to have_http_status(201) + + expect(json_response['name']).to eq('feature1') + expect(json_response['commit']['id']).to eq(branch_sha) + end + + it "denies for user without push access" do + post v3_api("/projects/#{project.id}/repository/branches", user2), + branch_name: branch_name, + ref: branch_sha + expect(response).to have_http_status(403) + end + + it 'returns 400 if branch name is invalid' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new design', + ref: branch_sha + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Branch name is invalid') + end + + it 'returns 400 if branch already exists' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design1', + ref: branch_sha + expect(response).to have_http_status(201) + + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design1', + ref: branch_sha + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Branch already exists') + end + + it 'returns 400 if ref name is invalid' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design3', + ref: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Invalid reference name') + end + end end 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/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 1941ca0d7d8..51021eec63c 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -439,6 +439,12 @@ describe API::V3::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get v3_api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index 4819269d69f..9233e9621bf 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -51,13 +51,6 @@ describe API::V3::Triggers do expect(pipeline.builds.size).to eq(5) end - it 'creates builds on webhook from other gitlab repository and branch' do - expect do - post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - expect(response).to have_http_status(201) - end - it 'returns bad request with no builds created if there\'s no commit for that ref' do post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) @@ -89,6 +82,27 @@ describe API::V3::Triggers do end end end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + + expect(response).to have_http_status(201) + end + end + end end describe 'GET /projects/:id/triggers' 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/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/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 65af4e13118..8567817147b 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -19,7 +19,7 @@ describe Ci::RetryBuildService, :services do erased_at].freeze IGNORE_ACCESSORS = - %i[type lock_version target_url gl_project_id deploy job_id base_tags + %i[type lock_version target_url base_tags commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id user_id].freeze diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb new file mode 100644 index 00000000000..3f548688c20 --- /dev/null +++ b/spec/services/create_branch_service_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe CreateBranchService, services: true do + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + context 'when repository is empty' do + let(:project) { create(:project_empty_repo) } + + it 'creates master branch' do + service.execute('my-feature', 'master') + + expect(project.repository.branch_exists?('master')).to be_truthy + end + + it 'creates my-feature branch' do + service.execute('my-feature', 'master') + + expect(project.repository.branch_exists?('my-feature')).to be_truthy + end + end + end +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/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 0768f644036..adfa75a524f 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do let(:commits) { Commit.decorate([commit_1], project) } it 'creates compare object with target branch as default branch' do - expect(merge_request.can_be_created).to eq(false) expect(merge_request.compare).to be_present expect(merge_request.target_branch).to eq(project.default_branch) end + + it 'allows the merge request to be created' do + expect(merge_request.can_be_created).to eq(true) + end end context 'same source and target branch' do diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 08829e4be70..b7a05907208 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -130,5 +130,15 @@ describe MergeRequests::GetUrlsService do }]) end end + + context 'when printing_merge_request_link_enabled is false' do + it 'returns empty array' do + project.update!(printing_merge_request_link_enabled: false) + + result = service.execute(existing_branch_changes) + + expect(result).to eq([]) + end + end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index ebbaea4e59a..f7240969588 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) @@ -795,7 +758,7 @@ describe NotificationService, services: true do update_custom_notification(:reopen_issue, @u_custom_global) end - it 'sends email to issue assignee and issue author' do + it 'sends email to issue notification recipients' do notification.reopen_issue(issue, @u_disabled) should_email(issue.assignee) @@ -809,6 +772,7 @@ describe NotificationService, services: true do should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) + should_not_email(@u_disabled) should_not_email(@u_lazy_participant) end @@ -818,6 +782,32 @@ describe NotificationService, services: true do let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } end end + + describe '#issue_moved' do + let(:new_issue) { create(:issue) } + + it 'sends email to issue notification recipients' do + notification.issue_moved(issue, new_issue, @u_disabled) + + should_email(issue.assignee) + should_email(issue.author) + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@watcher_and_subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@u_lazy_participant) + end + + it_behaves_like 'participating notifications' do + let(:participant) { create(:user, username: 'user-participant') } + let(:issuable) { issue } + let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } + end + end end describe 'Merge Requests' do @@ -855,20 +845,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 +1040,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 } @@ -1251,6 +1219,48 @@ describe NotificationService, services: true do end end + describe 'Pipelines' do + describe '#pipeline_finished' do + let(:project) { create(:project, :public) } + let(:current_user) { create(:user) } + let(:u_member) { create(:user) } + let(:u_other) { create(:user) } + + let(:commit) { project.commit } + let(:pipeline) do + create(:ci_pipeline, :success, + project: project, + user: current_user, + ref: 'refs/heads/master', + sha: commit.id, + before_sha: '00000000') + end + + before do + project.add_master(current_user) + project.add_master(u_member) + reset_delivered_emails! + end + + context 'without custom recipients' do + it 'notifies the pipeline user' do + notification.pipeline_finished(pipeline) + + should_only_email(current_user, kind: :bcc) + end + end + + context 'with custom recipients' do + it 'notifies the custom recipients' do + users = [u_member, u_other] + notification.pipeline_finished(pipeline, users.map(&:notification_email)) + + should_only_email(*users, kind: :bcc) + end + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index ab6e8f537ba..e5917bb0b7a 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do end end + context 'with blocked import_URL' do + it 'fails with localhost' do + project.import_url = 'https://localhost:9000/vim/vim.git' + + result = described_class.new(project, user).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to end_with 'Blocked import URL.' + end + + it 'fails with port 25' do + project.import_url = "https://github.com:25/vim/vim.git" + + result = described_class.new(project, user).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to end_with 'Blocked import URL.' + end + end + def stub_github_omniauth_provider provider = OpenStruct.new( 'name' => 'github', diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index db9f1231682..11037a4917b 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -5,6 +5,7 @@ describe SystemHooksService, services: true do let(:project) { create :project } let(:project_member) { create :project_member } let(:key) { create(:key, user: user) } + let(:deploy_key) { create(:key) } let(:group) { create(:group) } let(:group_member) { create(:group_member) } @@ -18,6 +19,8 @@ describe SystemHooksService, services: true do it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } + it { expect(event_data(deploy_key, :create)).to include(:key, :id) } + it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) } it do project.old_path_with_namespace = 'renamed_from_path' 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/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb new file mode 100644 index 00000000000..d9d59ec12ec --- /dev/null +++ b/spec/support/matchers/email_matchers.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :have_html_escaped_body_text do |expected| + match do |actual| + expect(actual).to have_body_text(ERB::Util.html_escape(expected)) + end +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/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb new file mode 100644 index 00000000000..3ee8f0f657e --- /dev/null +++ b/spec/support/target_branch_helpers.rb @@ -0,0 +1,16 @@ +module TargetBranchHelpers + def select_branch(name) + first('button.js-target-branch').click + wait_for_ajax + all('a[data-group="Branches"]').find do |el| + el.text == name + end.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 +end diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb new file mode 100644 index 00000000000..19036c7677c --- /dev/null +++ b/spec/tasks/tokens_spec.rb @@ -0,0 +1,21 @@ +require 'rake_helper' + +describe 'tokens rake tasks' do + let!(:user) { create(:user) } + + before do + Rake.application.rake_require 'tasks/tokens' + end + + describe 'reset_all task' do + it 'invokes create_hooks task' do + expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token } + end + end + + describe 'reset_all_email task' do + it 'invokes create_hooks task' do + expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token } + end + end +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 diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb deleted file mode 100644 index 542e674c150..00000000000 --- a/spec/workers/build_email_worker_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe BuildEmailWorker do - include EmailHelpers - include RepoHelpers - - let(:build) { create(:ci_build) } - let(:user) { create(:user) } - let(:data) { Gitlab::DataBuilder::Build.build(build) } - - subject { BuildEmailWorker.new } - - before do - allow(build).to receive(:execute_hooks).and_return(false) - build.success - end - - describe "#perform" do - it "sends mail" do - subject.perform(build.id, [user.email], data.stringify_keys) - - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include('Build success for') - expect(email.to).to eq([user.email]) - end - - it "gracefully handles an input SMTP error" do - reset_delivered_emails! - allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) - - subject.perform(build.id, [user.email], data.stringify_keys) - - expect(ActionMailer::Base.deliveries.count).to eq(0) - end - end -end |