diff options
Diffstat (limited to 'spec')
137 files changed, 3949 insertions, 1355 deletions
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb new file mode 100644 index 00000000000..b5fe40d0510 --- /dev/null +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Admin::RunnersController do + let(:runner) { create(:ci_runner) } + + before do + sign_in(create(:admin)) + end + + describe '#index' do + it 'lists all runners' do + get :index + + expect(response).to have_http_status(200) + end + end + + describe '#show' do + it 'shows a particular runner' do + get :show, id: runner.id + + expect(response).to have_http_status(200) + end + + it 'shows 404 for unknown runner' do + get :show, id: 0 + + expect(response).to have_http_status(404) + end + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, id: runner.id, runner: { description: new_desc } + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, id: runner.id + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb new file mode 100644 index 00000000000..566d8515198 --- /dev/null +++ b/spec/controllers/dashboard_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe DashboardController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET issues' do + it_behaves_like 'issuables list meta-data', :issue, :issues + end + + describe 'GET merge requests' do + it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + end +end diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 6bcfae0fc13..f7219690722 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -42,10 +42,9 @@ describe Profiles::KeysController do end describe "user with keys" do - before do - user.keys << create(:key) - user.keys << create(:another_key) - end + let!(:key) { create(:key, user: user) } + let!(:another_key) { create(:another_key, user: user) } + let!(:deploy_key) { create(:deploy_key, user: user) } it "does generally work" do get :get_keys, username: user.username @@ -53,16 +52,16 @@ describe Profiles::KeysController do expect(response).to be_success end - it "renders all keys separated with a new line" do + it "renders all non deploy keys separated with a new line" do get :get_keys, username: user.username - expect(response.body).not_to eq("") + expect(response.body).not_to eq('') expect(response.body).to eq(user.all_ssh_keys.join("\n")) - # Unique part of key 1 - expect(response.body).to match(/PWx6WM4lhHNedGfBpPJNPpZ/) - # Key 2 - expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/) + expect(response.body).to include(key.key.sub(' dummy@gitlab.com', '')) + expect(response.body).to include(another_key.key) + + expect(response.body).not_to include(deploy_key.key) end it "does not render the comment of the key" do diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb new file mode 100644 index 00000000000..58caf7999cf --- /dev/null +++ b/spec/controllers/profiles/notifications_controller_spec.rb @@ -0,0 +1,45 @@ +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/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 8f02003992a..7b3aa0491c7 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,8 +25,7 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars', - theme_id: '1' + dashboard: 'stars' ) patch :update, user: params, format: format @@ -41,8 +40,7 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars', - theme_id: '2' + dashboard: 'stars' }.with_indifferent_access expect(user).to receive(:update_attributes).with(prefs) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7ac1d62d1b1..84d119f1867 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -3,9 +3,12 @@ require 'spec_helper' describe Projects::EnvironmentsController do include ApiHelpers - let(:environment) { create(:environment) } - let(:project) { environment.project } - let(:user) { create(:user) } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:environment) do + create(:environment, name: 'production', project: project) + end before do project.team << [user, :master] @@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do end end - context 'when requesting JSON response' do - it 'responds with correct JSON' do - get :index, environment_params(format: :json) + context 'when requesting JSON response for folders' do + before do + create(:environment, project: project, + name: 'staging/review-1', + state: :available) + + create(:environment, project: project, + name: 'staging/review-2', + state: :available) + + create(:environment, project: project, + name: 'staging/review-3', + state: :stopped) + end + + let(:environments) { json_response['environments'] } + + context 'when requesting available environments scope' do + before do + get :index, environment_params(format: :json, scope: :available) + end + + it 'responds with a payload describing available environments' do + expect(environments.count).to eq 2 + expect(environments.first['name']).to eq 'production' + expect(environments.second['name']).to eq 'staging' + expect(environments.second['size']).to eq 2 + expect(environments.second['latest']['name']).to eq 'staging/review-2' + end - first_environment = json_response.first + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end + end - expect(first_environment).not_to be_empty - expect(first_environment['name']). to eq environment.name + context 'when requesting stopped environments scope' do + before do + get :index, environment_params(format: :json, scope: :stopped) + end + + it 'responds with a payload describing stopped environments' do + expect(environments.count).to eq 1 + expect(environments.first['name']).to eq 'staging' + expect(environments.first['size']).to eq 1 + expect(environments.first['latest']['name']).to eq 'staging/review-3' + end + + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 4b89381eb96..e576bf9ef79 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -24,6 +24,8 @@ describe Projects::IssuesController do project.team << [user, :developer] end + it_behaves_like "issuables list meta-data", :issue + it "returns index" do get :index, namespace_id: project.namespace.path, project_id: project.path diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 63780802cfa..f84f922ba5e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -147,6 +147,8 @@ describe Projects::MergeRequestsController do end describe 'GET index' do + let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + def get_merge_requests(page = nil) get :index, namespace_id: project.namespace.to_param, @@ -154,6 +156,8 @@ describe Projects::MergeRequestsController do state: 'opened', page: page.to_param end + it_behaves_like "issuables list meta-data", :merge_request + context 'when page param' do let(:last_page) { project.merge_requests.page().total_pages } let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -1139,15 +1143,15 @@ describe Projects::MergeRequestsController do end end - context 'when no special status for MR' do + context 'when MR does not have special state' do let(:merge_request) { create(:merge_request, source_project: project) } it 'returns an OK response' do expect(response).to have_http_status(:ok) end - it 'sets status to nil' do - expect(assigns(:status)).to be_nil + it 'sets status to success' do + expect(assigns(:status)).to eq(:success) expect(response).to render_template('merge') end end diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb new file mode 100644 index 00000000000..0fa249e4405 --- /dev/null +++ b/spec/controllers/projects/runners_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Projects::RunnersController do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:runner) { create(:ci_runner) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: runner + } + end + + before do + sign_in(user) + project.add_master(user) + project.runners << runner + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, params + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 570d9fa43f8..c9584ddf18c 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -4,6 +4,28 @@ describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } describe "GET show" do + context 'Content-Disposition security measures' do + let(:project) { create(:empty_project, :public) } + + context 'for PNG files' do + it 'returns Content-Disposition: inline' do + note = create(:note, :with_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + + expect(response['Content-Disposition']).to start_with('inline;') + end + end + + context 'for SVG files' do + it 'returns Content-Disposition: attachment' do + note = create(:note, :with_svg_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg' + + expect(response['Content-Disposition']).to start_with('attachment;') + end + end + end + context "when viewing a user avatar" do context "when signed in" do before do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 4f671091afc..a90534d10ba 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -139,5 +139,17 @@ FactoryGirl.define do build.save! end end + + trait :with_commit do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit, :without_author) + end + end + + trait :with_commit_and_author do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit) + end + end end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index ac6eb0a7897..89e260cf65b 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -8,5 +8,15 @@ FactoryGirl.define do initialize_with do new(git_commit, project) end + + after(:build) do |commit| + allow(commit).to receive(:author).and_return build(:author) + end + + trait :without_author do + after(:build) do |commit| + allow(commit).to receive(:author).and_return nil + end + end end end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index d69c5b38d0a..dd93b439b2b 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -2,10 +2,13 @@ FactoryGirl.define do factory :key do title key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com' end factory :deploy_key, class: 'DeployKey' do + key do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O96x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaCrzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy05qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz' + end end factory :personal_key do @@ -14,7 +17,7 @@ FactoryGirl.define do factory :another_key do key do - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ' end factory :another_deploy_key, class: 'DeployKey' do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index a21da7074f9..5c50cd7f4ad 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -97,7 +97,11 @@ FactoryGirl.define do end trait :with_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + end + + trait :with_svg_attachment do + attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") } end end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 275561502cd..b4e4cd97780 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -14,6 +14,10 @@ FactoryGirl.define do action { Todo::MENTIONED } end + trait :directly_addressed do + action { Todo::DIRECTLY_ADDRESSED } + end + trait :on_commit do commit_id RepoHelpers.sample_commit.id target_type "Commit" diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb new file mode 100644 index 00000000000..3f3c864ac2b --- /dev/null +++ b/spec/factories/wiki_directories.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :wiki_directory do + slug '/path_up_to/dir' + initialize_with { new(slug) } + end +end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index efa6cbe5bb1..4105f59e289 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -2,8 +2,26 @@ require 'ostruct' FactoryGirl.define do factory :wiki_page do + transient do + attrs do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + end + page { OpenStruct.new(url_path: 'some-name') } association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } + + before(:create) do |page, evaluator| + page.attributes = evaluator.attrs + end + + to_create do |page| + page.create + end end end diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 7fcfe5a54c7..340884fc986 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do end end end + + describe 'if a many users have been reported for abuse' do + let(:report_count) { AbuseReport.default_per_page + 3 } + + before do + report_count.times do + create(:abuse_report, user: create(:user)) + end + end + + describe 'in the abuse report view' do + it 'presents information about abuse report' do + visit admin_abuse_reports_path + + expect(page).to have_selector('.pagination') + expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) + end + end + end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 7225f38b7e5..1b25b51cfb2 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Welcome to your Issue Board!') end + it 'disables add issues button by default' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:disabled]).to eq true + end + it 'hides the blank state when clicking nevermind button' do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 3e0b6364e0d..35d090c4b7f 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -1,9 +1,11 @@ require 'spec_helper' -feature 'Contributions Calendar', js: true, feature: true do +feature 'Contributions Calendar', :feature, :js do include WaitForAjax + let(:user) { create(:user) } let(:contributed_project) { create(:project, :public) } + let(:issue_note) { create(:note, project: contributed_project) } # Ex/ Sunday Jan 1, 2016 date_format = '%A %b %-d, %Y' @@ -12,31 +14,31 @@ feature 'Contributions Calendar', js: true, feature: true do issue_params = { title: issue_title } def get_cell_color_selector(contributions) - contribution_cell = '.user-contrib-cell' - activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77'] - activity_colors_index = 0 - - if contributions > 0 && contributions < 10 - activity_colors_index = 1 - elsif contributions >= 10 && contributions < 20 - activity_colors_index = 2 - elsif contributions >= 20 && contributions < 30 - activity_colors_index = 3 - elsif contributions >= 30 - activity_colors_index = 4 - end + activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77] + # We currently don't actually test the cases with contributions >= 20 + activity_colors_index = + if contributions > 0 && contributions < 10 + 1 + elsif contributions >= 10 && contributions < 20 + 2 + elsif contributions >= 20 && contributions < 30 + 3 + elsif contributions >= 30 + 4 + else + 0 + end - "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']" + ".user-contrib-cell[fill='#{activity_colors[activity_colors_index]}']" end def get_cell_date_selector(contributions, date) - contribution_text = 'No contributions' - - if contributions === 1 - contribution_text = '1 contribution' - elsif contributions > 1 - contribution_text = "#{contributions} contributions" - end + contribution_text = + if contributions.zero? + 'No contributions' + else + "#{contributions} #{'contribution'.pluralize(contributions)}" + end "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']" end @@ -45,129 +47,155 @@ feature 'Contributions Calendar', js: true, feature: true do push_params = { project: contributed_project, action: Event::PUSHED, - author_id: @user.id, + author_id: user.id, data: { commit_count: 3 } } Event.create(push_params) end - def get_first_cell_content - find('.user-calendar-activities').text - end + def note_comment_contribution + note_comment_params = { + project: contributed_project, + action: Event::COMMENTED, + target: issue_note, + author_id: user.id + } - before do - login_as :user - visit @user.username - wait_for_ajax + Event.create(note_comment_params) end - it 'displays calendar', js: true do - expect(page).to have_css('.js-contrib-calendar') + def selected_day_activities + find('.user-calendar-activities').text end - describe 'select calendar day', js: true do - let(:cells) { page.all('.user-contrib-cell') } - let(:first_cell_content_before) { get_first_cell_content } + before do + login_as user + end + describe 'calendar day selection' do before do - cells[0].click + visit user.username wait_for_ajax - first_cell_content_before end - it 'displays calendar day activities', js: true do - expect(get_first_cell_content).not_to eq('') + it 'displays calendar' do + expect(page).to have_css('.js-contrib-calendar') end - describe 'select another calendar day', js: true do + describe 'select calendar day' do + let(:cells) { page.all('.user-contrib-cell') } + before do - cells[1].click + cells[0].click wait_for_ajax + @first_day_activities = selected_day_activities end - it 'displays different calendar day activities', js: true do - expect(get_first_cell_content).not_to eq(first_cell_content_before) + it 'displays calendar day activities' do + expect(selected_day_activities).not_to be_empty end - end - describe 'deselect calendar day', js: true do - before do - cells[0].click - wait_for_ajax + describe 'select another calendar day' do + before do + cells[1].click + wait_for_ajax + end + + it 'displays different calendar day activities' do + expect(selected_day_activities).not_to eq(@first_day_activities) + end end - it 'hides calendar day activities', js: true do - expect(get_first_cell_content).to eq('') + describe 'deselect calendar day' do + before do + cells[0].click + wait_for_ajax + end + + it 'hides calendar day activities' do + expect(selected_day_activities).to be_empty + end end end end - describe '1 calendar activity' do - before do - Issues::CreateService.new(contributed_project, @user, issue_params).execute - visit @user.username - wait_for_ajax + describe 'calendar daily activities' do + shared_context 'visit user page' do + before do + visit user.username + wait_for_ajax + end end - it 'displays calendar activity log', js: true do - expect(find('.content_list .event-note')).to have_content issue_title - end + shared_examples 'a day with activity' do |contribution_count:| + include_context 'visit user page' - it 'displays calendar activity square color for 1 contribution', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 1) - end + it 'displays calendar activity square color for 1 contribution' do + expect(page).to have_selector(get_cell_color_selector(contribution_count), count: 1) + end - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + it 'displays calendar activity square on the correct date' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1) + end end - end - describe '10 calendar activities' do - before do - (0..9).each do |i| - push_code_contribution() + describe '1 issue creation calendar activity' do + before do + Issues::CreateService.new(contributed_project, user, issue_params).execute end - visit @user.username - wait_for_ajax - end + it_behaves_like 'a day with activity', contribution_count: 1 - it 'displays calendar activity square color for 10 contributions', js: true do - expect(page).to have_selector(get_cell_color_selector(10), count: 1) - end + describe 'issue title is shown on activity page' do + include_context 'visit user page' - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(10, today), count: 1) + it 'displays calendar activity log' do + expect(find('.content_list .event-note')).to have_content issue_title + end + end end - end - describe 'calendar activity on two days' do - before do - push_code_contribution() - - Timecop.freeze(Date.yesterday) - Issues::CreateService.new(contributed_project, @user, issue_params).execute - Timecop.return + describe '1 comment calendar activity' do + before do + note_comment_contribution + end - visit @user.username - wait_for_ajax + it_behaves_like 'a day with activity', contribution_count: 1 end - it 'displays calendar activity squares for both days', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 2) - end + describe '10 calendar activities' do + before do + 10.times { push_code_contribution } + end - it 'displays calendar activity square for yesterday', js: true do - yesterday = Date.yesterday.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + it_behaves_like 'a day with activity', contribution_count: 10 end - it 'displays calendar activity square for today', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + describe 'calendar activity on two days' do + before do + push_code_contribution + + Timecop.freeze(Date.yesterday) do + Issues::CreateService.new(contributed_project, user, issue_params).execute + end + end + include_context 'visit user page' + + it 'displays calendar activity squares for both days' do + expect(page).to have_selector(get_cell_color_selector(1), count: 2) + end + + it 'displays calendar activity square for yesterday' do + yesterday = Date.yesterday.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + end + + it 'displays calendar activity square for today' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + end end end end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index 7d59fcac517..ae750be4d4a 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' -RSpec.describe 'Dashboard Active Tab', feature: true do +RSpec.describe 'Dashboard Active Tab', js: true, feature: true do before do login_as :user end shared_examples 'page has active tab' do |title| it "#{title} tab" do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - expect(find('.nav-sidebar li.active')).to have_content(title) + find('.global-dropdown-toggle').trigger('click') + expect(page).to have_selector('.global-dropdown-menu li.active', count: 1) + expect(find('.global-dropdown-menu li.active')).to have_content(title) end end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 41dcfe439c2..603076d7d37 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -36,7 +36,8 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do def expect_counters(issuable_type, count) dashboard_count = find('li.active span.badge') - nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count") + find('.global-dropdown-toggle').click + nav_count = find(".dashboard-shortcuts-#{issuable_type} span.badge") expect(nav_count).to have_content(count) expect(dashboard_count).to have_content(count) diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index d9be4e5dbdd..62a2c54c94c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do find('body').native.send_key('g') find('body').native.send_key('p') - ensure_active_main_tab('Projects') + check_page_title('Projects') find('body').native.send_key('g') find('body').native.send_key('i') - ensure_active_main_tab('Issues') + check_page_title('Issues') find('body').native.send_key('g') find('body').native.send_key('m') - ensure_active_main_tab('Merge Requests') + check_page_title('Merge Requests') end - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) + def check_page_title(title) + expect(find('.header-content .title')).to have_content(title) end end diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb new file mode 100644 index 00000000000..e31bc40adc3 --- /dev/null +++ b/spec/features/issuables/issuable_list_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +describe 'issuable list', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + issuable_types = [:issue, :merge_request] + + before do + project.add_user(user, :developer) + login_as(user) + issuable_types.each { |type| create_issuables(type) } + end + + issuable_types.each do |issuable_type| + it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do + control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count + + create_issuables(issuable_type) + + expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count) + end + + it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do + visit_issuable_list(issuable_type) + + expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1) + expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1) + expect(first('.fa-comments').find(:xpath, '..')).to have_content(2) + end + end + + def visit_issuable_list(issuable_type) + if issuable_type == :issue + visit namespace_project_issues_path(project.namespace, project) + else + visit namespace_project_merge_requests_path(project.namespace, project) + end + end + + def create_issuables(issuable_type) + 3.times do + if issuable_type == :issue + issuable = create(:issue, project: project, author: user) + else + issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + 2.times do + create(:note_on_issue, noteable: issuable, project: project, note: 'Test note') + end + + create(:award_emoji, :downvote, awardable: issuable) + create(:award_emoji, :upvote, awardable: issuable) + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 6f7046c8461..64f448a83b7 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -113,13 +113,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid author' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple authors' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -158,8 +156,7 @@ describe 'Filter issues', js: true, feature: true do end it 'sorting' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -182,13 +179,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid assignee' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple assignees' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -228,8 +223,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -253,8 +247,7 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid label' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple labels' do @@ -429,8 +422,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -456,13 +448,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by milestone containing special characters' do @@ -523,8 +513,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 93139dc9e94..7135565294b 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + it 'triggers autocomplete after selecting a slash command' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys('/as') + note.click + end + + find('.atwho-view li', text: '/assign').native.send_keys(:tab) + + user_item = find('.atwho-view li', text: user.username) + expect(user_item).to have_content(user.username) + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index c73065cdce1..eafcab6a0d7 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -10,10 +10,12 @@ feature 'Merge Request closing issues message', feature: true do :merge_request, :simple, source_project: project, - description: merge_request_description + description: merge_request_description, + title: merge_request_title ) end let(:merge_request_description) { 'Merge Request Description' } + let(:merge_request_title) { 'Merge Request Title' } before do project.team << [user, :master] @@ -45,8 +47,32 @@ feature 'Merge Request closing issues message', feature: true do end end - context 'closing some issues and mentioning, but not closing, others' do - let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + context 'closing some issues in title and mentioning, but not closing, others' do + let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") + end + end + + context 'closing issues using title but not mentioning any other issue' do + let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}") + end + end + + context 'mentioning issues using title but not closing them' do + let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") + end + end + + context 'closing some issues using title and mentioning, but not closing, others' do + let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } it 'does not display closing issue message' do expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index e853fb7e016..0832a3656a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Create New Merge Request', feature: true, js: true do + include WaitForVueResource + let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -99,6 +101,7 @@ feature 'Create New Merge Request', feature: true, js: true do page.within('.merge-request') do click_link 'Pipelines' + wait_for_vue_resource expect(page).to have_content "##{pipeline.id}" end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 957e913bf95..4ad944366c8 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -52,4 +52,19 @@ describe 'Merge request', :feature, :js do end end end + + context 'merge error' do + before do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + click_button 'Accept Merge Request' + wait_for_ajax + end + + it 'updates the MR widget' do + page.within('.mr-widget-body') do + expect(page).to have_content('Conflicts detected during merge') + end + end + end end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index a6b841c0210..15c8677fcd3 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do visit profile_preferences_path end - describe 'User changes their application theme', js: true do - let(:default) { Gitlab::Themes.default } - let(:theme) { Gitlab::Themes.by_id(5) } - - it 'creates a flash message' do - choose "user_theme_id_#{theme.id}" - - expect_preferences_saved_message - end - - it 'updates their preference' do - choose "user_theme_id_#{theme.id}" - - allowing_for_delay do - visit page.current_path - expect(page).to have_checked_field("user_theme_id_#{theme.id}") - end - end - - it 'reflects the changes immediately' do - expect(page).to have_selector("body.#{default.css_class}") - - choose "user_theme_id_#{theme.id}" - - expect(page).not_to have_selector("body.#{default.css_class}") - expect(page).to have_selector("body.#{theme.css_class}") - end - end - describe 'User changes their syntax highlighting theme', js: true do it 'creates a flash message' do choose 'user_color_scheme_id_5' 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 new file mode 100644 index 00000000000..e05fbb3715c --- /dev/null +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -0,0 +1,32 @@ +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/builds_spec.rb b/spec/features/projects/builds_spec.rb index f7e0115643e..2116721b224 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -109,6 +109,10 @@ feature 'Builds', :feature do expect(page).to have_content pipeline.git_commit_message expect(page).to have_content pipeline.git_author_name end + + it 'shows active build' do + expect(page).to have_selector('.build-job.active') + end end context "Job from other project" do @@ -271,7 +275,7 @@ feature 'Builds', :feature do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } - it 'shows a link to lastest deployment' do + it 'shows a link to latest deployment' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link('latest deployment') diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 227ccf9459c..02198ff3e41 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do expect(page).to have_link "Download '#{build.name}'", href: href end + + scenario 'download links have download attribute' do + expect(page).to have_selector('a', text: 'Download') + page.all('a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 6555b2fc6c1..8d1214dedb4 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -218,6 +218,14 @@ describe 'Pipelines', :feature, :js do expect(page).to have_link(with_artifacts.name) end + + it 'has download attribute on download links' do + find('.js-pipeline-dropdown-download').click + expect(page).to have_selector('a', text: 'Download') + page.all('.build-artifacts a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end context 'with artifacts expired' do @@ -277,6 +285,27 @@ describe 'Pipelines', :feature, :js do end end end + + context 'with pagination' do + before do + allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) + create(:ci_empty_pipeline, project: project) + end + + it 'should render pagination' do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination') + end + + it 'should render second page of pipelines' do + visit namespace_project_pipelines_path(project.namespace, project, page: '2') + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + end end describe 'POST /:project/pipelines' do diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 38fe2d92885..4eafac1acd8 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -20,9 +20,9 @@ feature 'Ref switcher', feature: true, js: true do input.set 'binary' wait_for_ajax - input.native.send_keys :down - input.native.send_keys :down - input.native.send_keys :enter + page.within '.dropdown-content ul' do + input.native.send_keys :enter + end end expect(page).to have_title 'binary-encoding' diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json index e6c1d9c9d84..6181b3ccc86 100644 --- a/spec/fixtures/api/schemas/user/login.json +++ b/spec/fixtures/api/schemas/user/login.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json index dbd5d32e89c..5587cfec61a 100644 --- a/spec/fixtures/api/schemas/user/public.json +++ b/spec/fixtures/api/schemas/user/public.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", @@ -32,14 +31,14 @@ "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { + "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "name": { "type": "string" }, - "state": { + "state": { "type": "string", - "enum": ["active", "blocked"] + "enum": ["active", "blocked"] }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" }, @@ -54,18 +53,17 @@ "organization": { "type": ["string", "null"] }, "last_sign_in_at": { "type": "date" }, "confirmed_at": { "type": ["date", "null"] }, - "theme_id": { "type": "integer" }, "color_scheme_id": { "type": "integer" }, "projects_limit": { "type": "integer" }, "current_sign_in_at": { "type": "date" }, - "identities": { + "identities": { "type": "array", "items": { "type": "object", "properties": { - "provider": { + "provider": { "type": "string", - "enum": ["github", "bitbucket", "google_oauth2"] + "enum": ["github", "bitbucket", "google_oauth2"] }, "extern_uid": { "type": ["number", "string"] } } @@ -74,6 +72,6 @@ "can_create_group": { "type": "boolean" }, "can_create_project": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, - "external": { "type": "boolean" } + "external": { "type": "boolean" } } } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 8b201f348f1..fd40fe99941 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -265,4 +265,9 @@ describe ApplicationHelper do expect(helper.render_markup('foo.adoc', content)).to eq('NOEL') end end + + describe '#active_when' do + it { expect(helper.active_when(true)).to eq('active') } + it { expect(helper.active_when(false)).to eq(nil) } + end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index dc07657e101..2cc0b40b2d0 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -40,6 +40,18 @@ describe PageLayoutHelper do end end + describe 'favicon' do + it 'defaults to favicon.ico' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + expect(helper.favicon).to eq 'favicon.ico' + end + + it 'has blue favicon for development' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + expect(helper.favicon).to eq 'favicon-blue.ico' + end + end + describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 1f02e06e312..f3e79cc7290 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -26,32 +26,6 @@ describe PreferencesHelper do end end - describe 'user_application_theme' do - context 'with a user' do - it "returns user's theme's css_class" do - stub_user(theme_id: 3) - - expect(helper.user_application_theme).to eq 'ui_green' - end - - it 'returns the default when id is invalid' do - stub_user(theme_id: Gitlab::Themes.count + 5) - - allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) - - expect(helper.user_application_theme).to eq 'ui_charcoal' - end - end - - context 'without a user' do - it 'returns the default theme' do - stub_user - - expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class - end - end - end - describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb new file mode 100644 index 00000000000..92c6f27a867 --- /dev/null +++ b/spec/helpers/wiki_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiHelper do + describe '#breadcrumb' do + context 'when the page is at the root level' do + it 'returns the capitalized page name' do + slug = 'page-name' + + expect(helper.breadcrumb(slug)).to eq('Page name') + end + end + + context 'when the page is inside a directory' do + it 'returns the capitalized name of each directory and of the page itself' do + slug = 'dir_1/page-name' + + expect(helper.breadcrumb(slug)).to eq('Dir_1 / Page name') + end + end + end +end diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 1541037888f..0e4c2c560cc 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -5,73 +5,83 @@ require('~/behaviors/quick_submit'); (function() { describe('Quick Submit behavior', function() { var keydownEvent; - preloadFixtures('static/behaviors/quick_submit.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); beforeEach(function() { - loadFixtures('static/behaviors/quick_submit.html.raw'); + loadFixtures('issues/open-issue.html.raw'); $('form').submit(function(e) { // Prevent a form submit from moving us off the testing page return e.preventDefault(); }); - return this.spies = { + this.spies = { submit: spyOnEvent('form', 'submit') }; + + this.textarea = $('.js-quick-submit textarea').first(); }); it('does not respond to other keyCodes', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ keyCode: 32 })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to Enter alone', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ ctrlKey: false, metaKey: false })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to repeated events', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ repeat: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); - it('disables submit buttons', function() { - $('textarea').trigger(keydownEvent()); - expect($('input[type=submit]')).toBeDisabled(); - return expect($('button[type=submit]')).toBeDisabled(); + it('disables input of type submit', function() { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); + }); + it('disables button of type submit', function() { + // button doesn't exist in fixture, add it manually + const submitButton = $('<button type="submit">Submit it</button>'); + submitButton.insertAfter(this.textarea); + + this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); }); // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { it('responds to Meta+Enter', function() { - $('input.quick-submit-input').trigger(keydownEvent()); + this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); it('excludes other modifier keys', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ altKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ ctrlKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ shiftKey: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); } else { it('responds to Ctrl+Enter', function() { - $('input.quick-submit-input').trigger(keydownEvent()); + this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); it('excludes other modifier keys', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ altKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ metaKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ shiftKey: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index a958ac76e66..631fca06514 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -4,18 +4,19 @@ require('~/behaviors/requires_input'); (function() { describe('requiresInput', function() { - preloadFixtures('static/behaviors/requires_input.html.raw'); + preloadFixtures('branches/new_branch.html.raw'); beforeEach(function() { - return loadFixtures('static/behaviors/requires_input.html.raw'); + loadFixtures('branches/new_branch.html.raw'); + this.submitButton = $('button[type="submit"]'); }); it('disables submit when any field is required', function() { $('.js-requires-input').requiresInput(); - return expect($('.submit')).toBeDisabled(); + return expect(this.submitButton).toBeDisabled(); }); it('enables submit when no field is required', function() { $('*[required=required]').removeAttr('required'); $('.js-requires-input').requiresInput(); - return expect($('.submit')).not.toBeDisabled(); + return expect(this.submitButton).not.toBeDisabled(); }); it('enables submit when all required fields are pre-filled', function() { $('*[required=required]').remove(); @@ -25,9 +26,9 @@ require('~/behaviors/requires_input'); it('enables submit when all required fields receive input', function() { $('.js-requires-input').requiresInput(); $('#required1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#optional1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#required2').val('input2').change(); $('#required3').val('input3').change(); $('#required4').val('input4').change(); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 index 789f5dc9f49..94973419979 100644 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -1,10 +1,10 @@ -require('~/commit/pipelines/pipelines_store'); +const PipelinesStore = require('~/commit/pipelines/pipelines_store'); describe('Store', () => { let store; beforeEach(() => { - store = new gl.commits.pipelines.PipelinesStore(); + store = new PipelinesStore(); }); // unregister intervals and event handlers diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 deleted file mode 100644 index c0bdb89ed63..00000000000 --- a/spec/javascripts/dashboard_spec.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-new */ - -require('~/sidebar'); -require('~/lib/utils/text_utility'); - -((global) => { - describe('Dashboard', () => { - const fixtureTemplate = 'static/dashboard.html.raw'; - - function todosCountText() { - return $('.js-todos-count').text(); - } - - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - new global.Sidebar(); - }); - - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); - expect(todosCountText()).toEqual('5'); - }); - - it('should display todos-count with delimiter', () => { - triggerToggle(1000); - expect(todosCountText()).toEqual('1,000'); - - triggerToggle(1000000); - expect(todosCountText()).toEqual('1,000,000'); - }); - }); -})(window.gl); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index b1838045a06..850586f9f3a 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_actions'); +const ActionsComponent = require('~/environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -19,7 +19,7 @@ describe('Actions Component', () => { }, ]; - const component = new window.gl.environmentsList.ActionsComponent({ + const component = new ActionsComponent({ el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, @@ -47,7 +47,7 @@ describe('Actions Component', () => { }, ]; - const component = new window.gl.environmentsList.ActionsComponent({ + const component = new ActionsComponent({ el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index a6a587e69f5..393dbb5aae0 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_external_url'); +const ExternalUrlComponent = require('~/environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -8,7 +8,7 @@ describe('External URL Component', () => { it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; - const component = new window.gl.environmentsList.ExternalUrlComponent({ + const component = new ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index e6fb0d00290..7fea80ed799 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,5 +1,5 @@ window.timeago = require('timeago.js'); -require('~/environments/components/environment_item'); +const EnvironmentItem = require('~/environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); @@ -14,33 +14,16 @@ describe('Environment item', () => { beforeEach(() => { mockItem = { name: 'review', - children: [ - { - name: 'review-app', - id: 1, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - { - name: 'production', - id: 2, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - ], + folderName: 'review', + size: 3, + isFolder: true, + environment_path: 'url', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, - toggleRow: () => {}, canCreateDeployment: false, canReadEnvironment: true, }, @@ -53,7 +36,7 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); }); }); @@ -63,8 +46,8 @@ describe('Environment item', () => { beforeEach(() => { environment = { - id: 31, name: 'production', + size: 1, state: 'stopped', external_url: 'http://external.com', environment_type: null, @@ -125,11 +108,10 @@ describe('Environment item', () => { updated_at: '2016-11-10T15:55:58.778Z', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: environment, - toggleRow: () => {}, canCreateDeployment: true, canReadEnvironment: true, }, diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 043b8708a6e..4a596baad09 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_rollback'); +const RollbackComponent = require('~/environments/components/environment_rollback'); describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Rollback Component', () => { }); it('Should link to the provided retryUrl', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -22,7 +22,7 @@ describe('Rollback Component', () => { }); it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -34,7 +34,7 @@ describe('Rollback Component', () => { }); it('Should render Rollback label when isLastDeployment is false', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 87eda136122..edd0cad32d0 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -1,9 +1,7 @@ -/* global Vue, environment */ - +const Vue = require('vue'); require('~/flash'); -require('~/environments/stores/environments_store'); -require('~/environments/components/environment'); -require('./mock_data'); +const EnvironmentsComponent = require('~/environments/components/environment'); +const { environment } = require('./mock_data'); describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); @@ -33,11 +31,8 @@ describe('Environment', () => { }); it('should render the empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { @@ -54,15 +49,30 @@ describe('Environment', () => { }); }); - describe('with environments', () => { + describe('with paginated environments', () => { const environmentsResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([environment]), { + next(request.respondWith(JSON.stringify({ + environments: [environment], + stopped_count: 1, + available_count: 0, + }), { status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, })); }; beforeEach(() => { Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + }); }); afterEach(() => { @@ -72,13 +82,6 @@ describe('Environment', () => { }); it('should render a table with environments', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, - }); - setTimeout(() => { expect( component.$el.querySelectorAll('table tbody tr').length, @@ -86,6 +89,59 @@ describe('Environment', () => { done(); }, 0); }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); }); }); @@ -107,11 +163,8 @@ describe('Environment', () => { }); it('should render empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 2dfce5ba824..5ca65b1debc 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_stop'); +const StopComponent = require('~/environments/components/environment_stop'); describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Stop Component', () => { loadFixtures('static/environments/element.html.raw'); stopURL = '/stop'; - component = new window.gl.environmentsList.StopComponent({ + component = new StopComponent({ el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6 new file mode 100644 index 00000000000..be4330b5012 --- /dev/null +++ b/spec/javascripts/environments/environment_table_spec.js.es6 @@ -0,0 +1,30 @@ +const EnvironmentTable = require('~/environments/components/environments_table'); + +describe('Environment item', () => { + preloadFixtures('static/environments/element.html.raw'); + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + it('Should render a table', () => { + const mockItem = { + name: 'review', + size: 3, + isFolder: true, + latest: { + environment_path: 'url', + }, + }; + + const component = new EnvironmentTable({ + el: document.querySelector('.test-dom-element'), + propsData: { + environments: [{ mockItem }], + canCreateDeployment: false, + canReadEnvironment: true, + }, + }); + + expect(component.$el.tagName).toEqual('TABLE'); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 9a8300d3832..77e182b3830 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,70 +1,58 @@ -/* global environmentsList */ - -require('~/environments/stores/environments_store'); -require('./mock_data'); +const Store = require('~/environments/stores/environments_store'); +const { environmentsList, serverData } = require('./mock_data'); (() => { describe('Store', () => { + let store; + beforeEach(() => { - gl.environmentsList.EnvironmentsStore.create(); + store = new Store(); }); it('should start with a blank state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); + expect(store.state.environments.length).toEqual(0); + expect(store.state.stoppedCounter).toEqual(0); + expect(store.state.availableCounter).toEqual(0); + expect(store.state.paginationInformation).toEqual({}); }); - describe('store environments', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should count stopped environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1); - }); - - it('should count available environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3); - }); - - it('should store environments with same environment_type as sibilings', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3); - - const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments - .filter(env => env.children && env.children.length > 0); - - expect(parentFolder[0].children.length).toBe(2); - expect(parentFolder[0].children[0].environment_type).toBe('review'); - expect(parentFolder[0].children[1].environment_type).toBe('review'); - expect(parentFolder[0].children[0].name).toBe('test-environment'); - expect(parentFolder[0].children[1].name).toBe('test-environment-1'); - }); - - it('should sort the environments alphabetically', () => { - const { environments } = gl.environmentsList.EnvironmentsStore.state; - - expect(environments[0].name).toBe('production'); - expect(environments[1].name).toBe('review'); - expect(environments[1].children[0].name).toBe('test-environment'); - expect(environments[1].children[1].name).toBe('test-environment-1'); - expect(environments[2].name).toBe('review_app'); - }); + it('should store environments', () => { + store.storeEnvironments(serverData); + expect(store.state.environments.length).toEqual(serverData.length); + expect(store.state.environments[0]).toEqual(environmentsList[0]); }); - describe('toggleFolder', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should toggle the open property for the given environment', () => { - gl.environmentsList.EnvironmentsStore.toggleFolder('review'); + it('should store available count', () => { + store.storeAvailableCount(2); + expect(store.state.availableCounter).toEqual(2); + }); - const { environments } = gl.environmentsList.EnvironmentsStore.state; - const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); + it('should store stopped count', () => { + store.storeStoppedCount(2); + expect(store.state.stoppedCounter).toEqual(2); + }); - expect(environment[0].isOpen).toBe(true); - }); + it('should store pagination information', () => { + 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.setPagination(pagination); + expect(store.state.paginationInformation).toEqual(expectedResult); }); }); })(); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 new file mode 100644 index 00000000000..d1335b5b304 --- /dev/null +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 @@ -0,0 +1,202 @@ +const Vue = require('vue'); +require('~/flash'); +const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); +const { environmentsList } = require('../mock_data'); + +describe('Environments Folder View', () => { + preloadFixtures('static/environments/environments_folder_view.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/environments_folder_view.html.raw'); + window.history.pushState({}, null, 'environments/folders/build'); + }); + + let component; + + describe('successfull request', () => { + const environmentsResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }), { + status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsResponseInterceptor, + ); + }); + + it('should render a table with environments', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('table tbody tr').length, + ).toEqual(2); + done(); + }, 0); + }); + + it('should render available tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('1'); + done(); + }, 0); + }); + + it('should render parent folder name', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-folder-name').textContent, + ).toContain('Environments / build'); + done(); + }, 0); + }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const environmentsErrorResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsErrorResponseInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsErrorResponseInterceptor, + ); + }); + + it('should not render a table', (done) => { + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + + setTimeout(() => { + expect( + component.$el.querySelector('table'), + ).toBe(null); + done(); + }, 0); + }); + + it('should render available tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 80e1cbc6f4d..5c395c6b2d8 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,153 +1,92 @@ - const environmentsList = [ { - id: 31, - name: 'production', + name: 'DEV', + size: 1, + id: 7, state: 'available', - external_url: 'https://www.gitlab.com', - environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', - }, - { - id: 32, - name: 'review_app', - state: 'stopped', - external_url: 'https://www.gitlab.com', + external_url: null, environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, + last_deployment: null, 'stop_action?': false, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', }, { - id: 33, - name: 'test-environment', + folderName: 'build', + size: 5, + id: 12, + name: 'build/update-README', state: 'available', - environment_type: 'review', + external_url: null, + environment_type: 'build', last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', }, +]; + +const serverData = [ { - id: 34, - name: 'test-environment-1', - state: 'available', - environment_type: 'review', - last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, + }, + { + name: 'build', + size: 5, + latest: { + id: 12, + name: 'build/update-README', + state: 'available', + external_url: null, + environment_type: 'build', + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', + }, }, ]; -window.environmentsList = environmentsList; - const environment = { - id: 4, - name: 'production', - state: 'available', - external_url: 'http://production.', - environment_type: null, - last_deployment: {}, - 'stop_action?': false, - environment_path: '/root/review-app/environments/4', - stop_path: '/root/review-app/environments/4/stop', - created_at: '2016-12-16T11:51:04.690Z', - updated_at: '2016-12-16T12:04:51.133Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, }; -window.environment = environment; +module.exports = { + environmentsList, + environment, + serverData, +}; diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index 009b68d5d1c..0c35cdd778e 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1 +1,2 @@ *.html.raw +*.json diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml deleted file mode 100644 index dc2ceed42f4..00000000000 --- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%form.js-quick-submit{ action: '/foo' } - %input{ type: 'text', class: 'quick-submit-input'} - %textarea - - %input{ type: 'submit'} Submit - %button.btn{ type: 'submit' } Submit diff --git a/spec/javascripts/fixtures/behaviors/requires_input.html.haml b/spec/javascripts/fixtures/behaviors/requires_input.html.haml deleted file mode 100644 index c3f905e912e..00000000000 --- a/spec/javascripts/fixtures/behaviors/requires_input.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%form.js-requires-input - %input{type: 'text', id: 'required1', required: 'required'} - %input{type: 'text', id: 'required2', required: 'required'} - %input{type: 'text', id: 'required3', required: 'required', value: 'Pre-filled'} - %input{type: 'text', id: 'optional1'} - - %textarea{id: 'required4', required: 'required'} - %textarea{id: 'optional2'} - - %select{id: 'required5', required: 'required'} - %option Zero - %option{value: '1'} One - %select{id: 'optional3', required: 'required'} - %option Zero - %option{value: '1'} One - - %button.submit{type: 'submit', value: 'Submit'} - %input.submit{type: 'submit', value: 'Submit'} diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb new file mode 100644 index 00000000000..0e7c2351b66 --- /dev/null +++ b/spec/javascripts/fixtures/branches.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('branches/') + end + + before(:each) do + sign_in(admin) + end + + it 'branches/new_branch.html.raw' do |example| + get :new, + namespace_id: project.namespace.to_param, + project_id: project.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml deleted file mode 100644 index 32446acfd60..00000000000 --- a/spec/javascripts/fixtures/dashboard.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -%ul.nav.nav-sidebar - %li.home.active - %a.dashboard-shortcuts-projects - %span - Projects - %li - %a - %span - Todos - %span.count.js-todos-count - 1 - %li - %a.dashboard-shortcuts-activity - %span - Activity - %li - %a - %span - Groups - %li - %a - %span - Milestones - %li - %a.dashboard-shortcuts-issues - %span - Issues - %span - 1 - %li - %a.dashboard-shortcuts-merge_requests - %span - Merge Requests - %li - %a - %span - Snippets - %li - %a - %span - Help - %li - %a - %span - Profile Settings diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml new file mode 100644 index 00000000000..aceec139730 --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml @@ -0,0 +1,7 @@ +%div + #environments-folder-list-view{ data: { "can-create-deployment" => "true", + "can-read-environment" => "true", + "css-class" => "", + "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), + "play-icon-svg" => custom_icon("icon_play") } } diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml deleted file mode 100644 index f397f69e753..00000000000 --- a/spec/javascripts/fixtures/header.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -%header.navbar.navbar-gitlab.nav_header_class - .container-fluid - .header-content - %button.side-nav-toggle - %span.sr-only - Toggle navigation - %i.fa.fa-bars - %button.navbar-toggle - %span.sr-only - Toggle navigation - %i.fa.fa-ellipsis-v - .navbar-collapse.collapse - %ui.nav.navbar-nav - %li.hidden-sm.hidden-xs - %li.visible-sm.visible-xs - %li - %a - %i.fa.fa-bell.fa-fw - %span.badge.todos-pending-count - %li - %a - %i.fa.fa-plus.fa-fw - %li.header-user.dropdown - %a - %img - %span.caret - .dropdown-menu-nav - .dropdown-menu-align-right - %ul - %li - %a.profile-link - %li - %a - %li.divider - %li.sign-out-link diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml deleted file mode 100644 index 68678c3d7e3..00000000000 --- a/spec/javascripts/fixtures/merge_request_tabs.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%ul.nav.nav-tabs.merge-request-tabs - %li.notes-tab - %a{href: '/foo/bar/merge_requests/1', data: {target: 'div#notes', action: 'notes', toggle: 'tab'}} - Discussion - %li.commits-tab - %a{href: '/foo/bar/merge_requests/1/commits', data: {target: 'div#commits', action: 'commits', toggle: 'tab'}} - Commits - %li.diffs-tab - %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'}} - Diffs - -.tab-content - #notes.notes.tab-pane - Notes Content - #commits.commits.tab-pane - Commits Content - #diffs.diffs.tab-pane - Diffs Content - -.mr-loading-status - .loading - Loading Animation diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb new file mode 100644 index 00000000000..62984097099 --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('merge_requests/') + end + + before(:each) do + sign_in(admin) + end + + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| + merge_request = create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') + render_merge_request(example.description, merge_request) + end + + private + + def render_merge_request(fixture_file_name, merge_request) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.to_param + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml deleted file mode 100644 index f06629e5ecc..00000000000 --- a/spec/javascripts/fixtures/new_branch.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%form.js-create-branch-form - %input.js-branch-name - .js-branch-name-error - %input{id: "ref"} diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json deleted file mode 100644 index 62c2387d515..00000000000 --- a/spec/javascripts/fixtures/todos.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "count": 1, - "delete_path": "/dashboard/todos/1" -}
\ No newline at end of file diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb new file mode 100644 index 00000000000..2c08b06ea9e --- /dev/null +++ b/spec/javascripts/fixtures/todos.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'Todos (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) } + + before(:all) do + clean_frontend_fixtures('todos/') + end + + describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.json' do |example| + post :create, + namespace_id: namespace.path, + project_id: project.path, + issuable_type: 'issue', + issuable_id: issue_2.id, + format: 'json' + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 317f38c5888..c207fb00a47 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -139,6 +139,14 @@ require('~/lib/utils/url_utility'); this.dropdownButtonElement.click(); }); + it('should show loading indicator while search results are being fetched by backend', () => { + const dropdownMenu = document.querySelector('.dropdown-menu'); + + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + remoteCallback(); + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + }); + it('should not focus search input while remote task is not complete', () => { expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); remoteCallback(); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index cecebb0b038..2b263b71b7d 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -6,7 +6,7 @@ require('~/lib/utils/text_utility'); (function() { describe('Header', function() { var todosPendingCount = '.todos-pending-count'; - var fixtureTemplate = 'static/header.html.raw'; + var fixtureTemplate = 'issues/open-issue.html.raw'; function isTodosCountHidden() { return $(todosPendingCount).hasClass('hidden'); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 5b0b7aa7903..beb544468ef 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -105,6 +105,7 @@ require('~/issue'); expectIssueState(false); expect($btnClose).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(0); }); it('fails to close an issue with success:false', function() { @@ -121,6 +122,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', false); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); it('fails to closes an issue with HTTP error', function() { @@ -135,6 +137,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', true); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); }); @@ -159,6 +162,7 @@ require('~/issue'); expectIssueState(true); expect($btnReopen).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(1); }); }); }).call(this); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 006ede21093..f4d3e77e515 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -108,6 +108,30 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); + describe('gl.utils.isMetaClick', () => { it('should identify meta click on Windows/Linux', () => { const e = { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 92a0f1c05f7..5b0c124962c 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -25,7 +25,7 @@ require('vendor/jquery.scrollTo'); }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('static/merge_request_tabs.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -41,7 +41,7 @@ require('vendor/jquery.scrollTo'); describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); - loadFixtures('static/merge_request_tabs.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); it('shows the first tab when action is show', function () { @@ -62,19 +62,47 @@ require('vendor/jquery.scrollTo'); }); }); describe('#opensInNewTab', function () { - var commitsLink; var tabUrl; + var windowTarget = '_blank'; beforeEach(function () { - commitsLink = '.commits-tab li a'; - tabUrl = $(commitsLink).attr('href'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + + tabUrl = $('.commits-tab a').attr('href'); spyOn($.fn, 'attr').and.returnValue(tabUrl); }); + + describe('meta click', () => { + beforeEach(function () { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + }); + + it('opens page when commits link is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a').click(); + }); + + it('opens page when commits badge is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + }); + }); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -87,7 +115,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -100,7 +128,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 9b657868523..1d014502c2a 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -8,7 +8,7 @@ require('~/new_branch_form'); describe('Branch', function() { return describe('create a new branch', function() { var expectToHaveError, fillNameWith; - preloadFixtures('static/new_branch.html.raw'); + preloadFixtures('branches/new_branch.html.raw'); fillNameWith = function(value) { return $('.js-branch-name').val(value).trigger('blur'); }; @@ -16,7 +16,7 @@ require('~/new_branch_form'); return expect($('.js-branch-name-error span').text()).toEqual(error); }; beforeEach(function() { - loadFixtures('static/new_branch.html.raw'); + loadFixtures('branches/new_branch.html.raw'); $('form').on('submit', function(e) { return e.preventDefault(); }); diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6 deleted file mode 100644 index 24833b4eb57..00000000000 --- a/spec/javascripts/project_dashboard_spec.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -require('~/sidebar'); - -(() => { - describe('Project dashboard page', () => { - let $pageWithSidebar = null; - let $sidebarToggle = null; - let sidebar = null; - const fixtureTemplate = 'projects/dashboard.html.raw'; - - const assertSidebarStateExpanded = (shouldBeExpanded) => { - expect(sidebar.isExpanded).toBe(shouldBeExpanded); - expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded); - }; - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - - $pageWithSidebar = $('.page-with-sidebar'); - $sidebarToggle = $('.toggle-nav-collapse'); - - // otherwise instantiating the Sidebar for the second time - // won't do anything, as the Sidebar is a singleton class - gl.Sidebar.singleton = null; - sidebar = new gl.Sidebar(); - }); - - it('can show the sidebar when the toggler is clicked', () => { - assertSidebarStateExpanded(false); - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - }); - - it('should dismiss the sidebar when clone button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const cloneButton = $('.project-clone-holder a.clone-dropdown-btn'); - cloneButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when download button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const downloadButton = $('.project-action-button .btn:has(i.fa-download)'); - downloadButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when add button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const addButton = $('.project-action-button .btn:has(i.fa-plus)'); - addButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when notification button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const notifButton = $('.js-notification-toggle-btns .notifications-btn'); - notifButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the body', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('body').click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the project description header', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('.project-home-panel').click(); - assertSidebarStateExpanded(false); - }); - }); -})(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index f7636865aa1..9284af8a8d9 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,7 +34,7 @@ require('~/extensions/jquery.js'); describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); - loadJSONFixtures('todos.json'); + loadJSONFixtures('todos/todos.json'); beforeEach(function() { loadFixtures(fixtureName); @@ -64,7 +64,7 @@ require('~/extensions/jquery.js'); }); it('should broadcast todo:toggle event when add todo clicked', function() { - var todos = getJSONFixture('todos.json'); + var todos = getJSONFixture('todos/todos.json'); spyOn(jQuery, 'ajax').and.callFake(function() { var d = $.Deferred(); var response = todos; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 030d2de090a..ca707d872a4 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -42,3 +42,38 @@ 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 + const troubleMakers = [ + './blob_edit/blob_edit_bundle.js', + './cycle_analytics/components/stage_plan_component.js', + './cycle_analytics/components/stage_staging_component.js', + './cycle_analytics/components/stage_test_component.js', + './diff_notes/components/jump_to_discussion.js', + './diff_notes/components/resolve_count.js', + './merge_conflicts/components/inline_conflict_lines.js', + './merge_conflicts/components/parallel_conflict_lines.js', + './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; + } + + it(`includes '${path}'`, function () { + try { + sourceFiles(path); + } catch (err) { + if (troubleMakers.indexOf(path) === -1) { + expect(err).toBeNull(); + } + } + }); + }); +}); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js new file mode 100644 index 00000000000..66e4fbd6304 --- /dev/null +++ b/spec/javascripts/todos_spec.js @@ -0,0 +1,63 @@ +require('~/todos'); +require('~/lib/utils/common_utils'); + +describe('Todos', () => { + preloadFixtures('todos/todos.html.raw'); + let todoItem; + + beforeEach(() => { + loadFixtures('todos/todos.html.raw'); + todoItem = document.querySelector('.todos-list .todo'); + + return new gl.Todos(); + }); + + describe('goToTodoUrl', () => { + it('opens the todo url', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(todoLink); + done(); + }); + + todoItem.click(); + }); + + describe('meta click', () => { + let visitUrlSpy; + + beforeEach(() => { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + }); + + it('opens the todo url in another tab', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(window, 'open').and.callFake((url, target) => { + expect(todoLink).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + todoItem.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + + it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { + const avatarImage = todoItem.querySelector('img'); + const avatarUrl = avatarImage.parentElement.getAttribute('href'); + + spyOn(window, 'open').and.callFake((url, target) => { + expect(avatarUrl).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + avatarImage.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index e84f0dcfe67..dd495cb43bc 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -34,7 +34,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '1' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the previous page', () => { @@ -55,7 +55,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Prev' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the next page', () => { @@ -76,7 +76,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Next' } }); expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the last page', () => { @@ -97,7 +97,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Last >>' } }); expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the first page', () => { @@ -118,7 +118,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '<< First' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should do nothing', () => { @@ -139,7 +139,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '...' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); }); diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb new file mode 100644 index 00000000000..580450eef1e --- /dev/null +++ b/spec/lib/additional_email_headers_interceptor_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe AdditionalEmailHeadersInterceptor do + it 'adds Auto-Submitted header' do + mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver + + expect(mail.header['To'].value).to eq('test@mail.com') + expect(mail.header['From'].value).to eq('info@mail.com') + expect(mail.header['Auto-Submitted'].value).to eq('auto-generated') + expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All') + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 456dbac0698..11607d4fb26 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - describe '#issues_per_Project' do + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do doc = Nokogiri::HTML.fragment('') @@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end end + + describe '.references_in' do + let(:merge_request) { create(:merge_request) } + + it 'yields valid references' do + expect do |b| + described_class.references_in(issue.to_reference, &b) + end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData) + end + + it "doesn't yield invalid references" do + expect do |b| + described_class.references_in('#0', &b) + end.not_to yield_control + end + + it "doesn't yield unsupported references" do + expect do |b| + described_class.references_in(merge_request.to_reference, &b) + end.not_to yield_control + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 3e1ac9fb2b2..d5d128c1907 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -112,6 +112,19 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end + context 'mentioning a nested group' do + it_behaves_like 'a reference containing an element node' + + let(:group) { create(:group, :nested) } + let(:reference) { group.to_reference } + + it 'links to the nested group' do + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + end + it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index ba199917f5c..bca57105d1d 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -41,6 +41,29 @@ module Gitlab render(input, context, asciidoc_opts) end end + + context "XSS" do + links = { + 'links' => { + input: 'link:mylink"onmouseover="alert(1)[Click Here]', + output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>" + }, + 'images' => { + input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', + output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>" + }, + 'pre' => { + input: '```mypre"><script>alert(3)</script>', + output: "<div>\n<div>\n<pre lang=\"mypre\">\"><code></code></pre>\n</div>\n</div>" + } + } + + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:input], context)).to eql data[:output] + end + end + end end def render(*args) diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb new file mode 100644 index 00000000000..c455cd9b942 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::BaseEventFetcher do + let(:max_events) { 2 } + let(:project) { create(:project) } + let(:user) { create(:user, :admin) } + let(:start_time_attrs) { Issue.arel_table[:created_at] } + let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] } + let(:options) do + { start_time_attrs: start_time_attrs, + end_time_attrs: end_time_attrs, + from: 30.days.ago } + end + + subject do + described_class.new(project: project, + stage: :issue, + options: options).fetch + end + + before do + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event| + event + end + + stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events) + + setup_events(count: 3) + end + + it 'limits the rows to the max number' do + expect(subject.count).to eq(max_events) + end + + def setup_events(count:) + count.times do + issue = create(:issue, project: project, created_at: 2.days.ago) + milestone = create(:milestone, project: project) + + issue.update(milestone: milestone) + create_merge_request_closing_issue(issue) + end + end +end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 6c71e98066b..91c43f2bdc0 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + + context 'commit author_url' do + context 'when no commit present' do + let(:build) { create(:ci_build) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit present but has no author' do + let(:build) { create(:ci_build, :with_commit) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit and author are present' do + let(:build) { create(:ci_build, :with_commit_and_author) } + + it 'sets to GitLab user url' do + expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username)) + end + end + end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 7fd25b9e5bf..e94ca4fcfd2 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#add_concurrent_index' do context 'outside a transaction' do before do - expect(model).to receive(:transaction_open?).and_return(false) - - unless Gitlab::Database.postgresql? - allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout) - end + allow(model).to receive(:transaction_open?).and_return(false) end context 'using PostgreSQL' do - before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end it 'creates the index concurrently' do expect(model).to receive(:add_index). @@ -59,6 +58,71 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#add_concurrent_foreign_key' do + context 'inside a transaction' do + it 'raises an error' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end.to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using MySQL' do + it 'creates a regular foreign key' do + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + + expect(model).to receive(:add_foreign_key). + with(:projects, :users, column: :user_id, on_delete: :cascade) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + it 'creates a concurrent foreign key' do + expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:execute).ordered.with(/NOT VALID/) + expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + end + end + + describe '#disable_statement_timeout' do + context 'using PostgreSQL' do + it 'disables statement timeouts' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:execute).with('SET statement_timeout TO 0') + + model.disable_statement_timeout + end + end + + context 'using MySQL' do + it 'does nothing' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).not_to receive(:execute) + + model.disable_statement_timeout + end + end + end + describe '#update_column_in_batches' do before do create_list(:empty_project, 5) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index b142b3a2781..f01c42aff91 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -5,6 +5,12 @@ class MigrationTest end describe Gitlab::Database, lib: true do + describe '.adapter_name' do + it 'returns the name of the adapter' do + expect(described_class.adapter_name).to be_an_instance_of(String) + end + end + # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do @@ -71,6 +77,54 @@ describe Gitlab::Database, lib: true do end end + describe '.with_connection_pool' do + it 'creates a new connection pool and disconnect it after used' do + closed_pool = nil + + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + expect(pool).to be_connected + + closed_pool = pool + end + + expect(closed_pool).not_to be_connected + end + + it 'disconnects the pool even an exception was raised' do + error = Class.new(RuntimeError) + closed_pool = nil + + begin + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + closed_pool = pool + + raise error.new('boom') + end + rescue error + end + + expect(closed_pool).not_to be_connected + end + end + + describe '.create_connection_pool' do + it 'creates a new connection pool with specific pool size' do + pool = described_class.create_connection_pool(5) + + expect(pool) + .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) + expect(pool.spec.config[:pool]).to eq(5) + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 53f7d244d88..20743811dab 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -2,14 +2,15 @@ require 'spec_helper' describe Gitlab::ImportExport, services: true do describe 'export filename' do - let(:project) { create(:empty_project, :public, path: 'project-path') } + let(:group) { create(:group, :nested) } + let(:project) { create(:empty_project, :public, path: 'project-path', namespace: group) } it 'contains the project path' do expect(described_class.export_filename(project: project)).to include(project.path) end it 'contains the namespace path' do - expect(described_class.export_filename(project: project)).to include(project.namespace.path) + expect(described_class.export_filename(project: project)).to include(project.namespace.full_path) end it 'does not go over a certain length' do diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json new file mode 100644 index 00000000000..a78836c3c34 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.light.json @@ -0,0 +1,48 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "labels": [ + { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +}
\ 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 0af13ba8e47..94a3b0fbba9 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -3,24 +3,28 @@ include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do - let(:user) { create(:user) } - let(:namespace) { create(:namespace, owner: user) } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } - let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } - let(:restored_project_json) { project_tree_restorer.restore } + before(:all) do + @user = create(:user) - before do - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + RSpec::Mocks.with_temporary_scope do + @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') + allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project) + @restored_project_json = project_tree_restorer.restore + end + end + + after(:all) do + @user.destroy! end context 'JSON' do it 'restores models based on JSON' do - expect(restored_project_json).to be true + expect(@restored_project_json).to be true end it 'restore correct project features' do - restored_project_json project = Project.find_by_path('project') expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) @@ -31,62 +35,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end it 'has the same label associated to two issues' do - restored_project_json - expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end it 'has milestones associated to two separate issues' do - restored_project_json - expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) end it 'creates a valid pipeline note' do - restored_project_json - expect(Ci::Pipeline.first.notes).not_to be_empty end it 'restores pipelines with missing ref' do - restored_project_json - expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end it 'restores the correct event with symbolised data' do - restored_project_json - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end it 'preserves updated_at on issues' do - restored_project_json - issue = Issue.where(description: 'Aliquam enim illo et possimus.').first expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') end it 'contains the merge access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.merge_access_levels).not_to be_empty end it 'contains the push access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.push_access_levels).not_to be_empty end context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } - before do - restored_project_json - end - it 'restores the event' do expect(event).not_to be_nil end @@ -99,77 +83,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ - expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) end it 'has labels associated to label links, associated to issues' do - restored_project_json - expect(Label.first.label_links.first.target).not_to be_nil end it 'has project labels' do - restored_project_json - expect(ProjectLabel.count).to eq(2) end it 'has no group labels' do - restored_project_json - expect(GroupLabel.count).to eq(0) end - context 'with group' do - let!(:project) do - create(:empty_project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: create(:group)) - end - - it 'has group labels' do - restored_project_json - - expect(GroupLabel.count).to eq(1) - end - - it 'has label priorities' do - restored_project_json - - expect(GroupLabel.first.priorities).not_to be_empty - end - end - it 'has a project feature' do - restored_project_json - - expect(project.project_feature).not_to be_nil + expect(@project.project_feature).not_to be_nil end it 'restores the correct service' do - restored_project_json - expect(CustomIssueTrackerService.first).not_to be_nil end context 'Merge requests' do - before do - restored_project_json - end - it 'always has the new project as a target' do - expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) end it 'has the same source project as originally if source/target are the same' do - expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project) end it 'has the new project as target if source/target differ' do - expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project) end it 'has no source if source/target differ' do @@ -177,39 +124,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original - - restored_project_json + context 'tokens are regenerated' do + it 'has a new CI trigger token' do + expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + end - expect(shared.errors.first).not_to include('test') - end + it 'has a new CI build token' do + expect(Ci::Build.where(token: 'abcd')).to be_empty end end + end + end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'Light JSON' do + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } - expect(restored_project_json).to be true - end - end + before do + allow(ImportExport).to receive(:project_filename).and_return('project.light.json') + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original - context 'tokens are regenerated' do - before do restored_project_json - end - it 'has a new CI trigger token' do - expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + expect(shared.errors.first).not_to include('test') end + end + end - it 'has a new CI build token' do - expect(Ci::Build.where(token: 'abcd')).to be_empty - end + context 'when there is an existing build with build token' do + it 'restores project json correctly' do + create(:ci_build, token: 'abcd') + + expect(restored_project_json).to be true + end + end + + context 'with group' do + let!(:project) do + create(:empty_project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + restored_project_json + end + + it 'has group labels' do + expect(GroupLabel.count).to eq(1) + end + + it 'has label priorities' do + expect(GroupLabel.first.priorities).not_to be_empty end end end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb new file mode 100644 index 00000000000..168a59e5139 --- /dev/null +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::RepoRestorer, services: true do + describe 'bundle a project Git repo' do + let(:user) { create(:user) } + let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project) { create(:empty_project) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } + let(:restorer) do + described_class.new(path_to_bundle: bundle_path, + shared: shared, + project: project) + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end + + it 'restores the repo successfully' do + expect(restorer.restore).to be true + end + + it 'has the webhooks' do + restorer.restore + + expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + end + end +end diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup.rb new file mode 100644 index 00000000000..8f5a353b381 --- /dev/null +++ b/spec/lib/gitlab/other_markup.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::OtherMarkup, lib: true do + context "XSS Checks" do + links = { + 'links' => { + file: 'file.rdoc', + input: 'XSS[JaVaScriPt:alert(1)]', + output: '<p><a>XSS</a></p>' + } + } + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:file], data[:input], context)).to eql data[:output] + end + end + end + + def render(*args) + described_class.render(*args) + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 6b689c41ef6..84cfd934fa0 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -42,14 +42,85 @@ describe Gitlab::ReferenceExtractor, lib: true do > @offteam }) + expect(subject.users).to match_array([]) end + describe 'directly addressed users' do + before do + @u_foo = create(:user, username: 'foo') + @u_foo2 = create(:user, username: 'foo2') + @u_foo3 = create(:user, username: 'foo3') + @u_foo4 = create(:user, username: 'foo4') + @u_foo5 = create(:user, username: 'foo5') + + @u_bar = create(:user, username: 'bar') + @u_bar2 = create(:user, username: 'bar2') + @u_bar3 = create(:user, username: 'bar3') + @u_bar4 = create(:user, username: 'bar4') + + @u_tom = create(:user, username: 'tom') + @u_tom2 = create(:user, username: 'tom2') + end + + context 'when a user is directly addressed' do + it 'accesses the user object which is mentioned in the beginning of the line' do + subject.analyze('@foo What do you think? cc: @bar, @tom') + + expect(subject.directly_addressed_users).to match_array([@u_foo]) + end + + it "doesn't access the user object if it's not mentioned in the beginning of the line" do + subject.analyze('What do you think? cc: @bar') + + expect(subject.directly_addressed_users).to be_empty + end + end + + context 'when multiple users are addressed' do + it 'accesses the user objects which are mentioned in the beginning of the line' do + subject.analyze('@foo @bar What do you think? cc: @tom') + + expect(subject.directly_addressed_users).to match_array([@u_foo, @u_bar]) + end + + it "doesn't access the user objects if they are not mentioned in the beginning of the line" do + subject.analyze('What do you think? cc: @foo @bar @tom') + + expect(subject.directly_addressed_users).to be_empty + end + end + + context 'when multiple users are addressed in different paragraphs' do + it 'accesses user objects which are mentioned in the beginning of each paragraph' do + subject.analyze <<-NOTE.strip_heredoc + @foo What do you think? cc: @tom + + - @bar can you please have a look? + + >>> + @foo2 what do you think? cc: @bar2 + >>> + + @foo3 @foo4 thank you! + + > @foo5 well done! + + 1. @bar3 Can you please check? cc: @tom2 + 2. @bar4 What do you this of this MR? + NOTE + + expect(subject.directly_addressed_users).to match_array([@u_foo, @u_foo3, @u_foo4]) + end + end + end + it 'accesses valid issue objects' do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") + expect(subject.issues).to match_array([@i0, @i1]) end @@ -58,6 +129,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict') subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") + expect(subject.merge_requests).to match_array([@m1, @m0]) end @@ -67,6 +139,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @l2 = create(:label) subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") + expect(subject.labels).to match_array([@l0, @l1]) end @@ -76,6 +149,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @s2 = create(:project_snippet) subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") + expect(subject.snippets).to match_array([@s0, @s1]) end @@ -127,6 +201,7 @@ describe Gitlab::ReferenceExtractor, lib: true do it 'handles project issue references' do subject.analyze("this refers issue #{issue.to_reference(project)}") + extracted = subject.issues expect(extracted.size).to eq(1) expect(extracted).to match_array([issue]) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c78cd30157e..089ec4e2737 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,47 +2,64 @@ require 'spec_helper' describe Gitlab::Regex, lib: true do - describe 'project path regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } - it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } - it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } + describe '.project_path_regex' do + subject { described_class.project_path_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } end - describe 'project name regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) } - it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) } - it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) } - it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) } - it { expect('Český název').to match(Gitlab::Regex.project_name_regex) } - it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } + describe '.project_name_regex' do + subject { described_class.project_name_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('GitLab CE') } + it { is_expected.to match('100 lines') } + it { is_expected.to match('gitlab.git') } + it { is_expected.to match('Český název') } + it { is_expected.to match('Dash – is this') } + it { is_expected.not_to match('?gitlab') } end - describe 'file name regex' do - it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) } + describe '.file_name_regex' do + subject { described_class.file_name_regex } + + it { is_expected.to match('foo@bar') } end - describe 'file path regex' do - it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } + describe '.file_path_regex' do + subject { described_class.file_path_regex } + + it { is_expected.to match('foo@/bar') } end - describe 'environment slug regex' do - def be_matched - match(Gitlab::Regex.environment_slug_regex) - end + describe '.environment_slug_regex' do + subject { described_class.environment_slug_regex } + + it { is_expected.to match('foo') } + it { is_expected.to match('foo-1') } + it { is_expected.not_to match('FOO') } + it { is_expected.not_to match('foo/1') } + it { is_expected.not_to match('foo.1') } + it { is_expected.not_to match('foo*1') } + it { is_expected.not_to match('9foo') } + it { is_expected.not_to match('foo-') } + end - it { expect('foo').to be_matched } - it { expect('foo-1').to be_matched } + describe 'NAMESPACE_REF_REGEX_STR' do + subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} } - it { expect('FOO').not_to be_matched } - it { expect('foo/1').not_to be_matched } - it { expect('foo.1').not_to be_matched } - it { expect('foo*1').not_to be_matched } - it { expect('9foo').not_to be_matched } - it { expect('foo-').not_to be_matched } + it { is_expected.to match('gitlab.org') } + it { is_expected.to match('gitlab.org/gitlab-git') } + it { is_expected.not_to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } + it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab git') } end end diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb index 1e4954c4af8..d7f77486b3e 100644 --- a/spec/lib/gitlab/slash_commands/extractor_spec.rb +++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb @@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do let(:original_msg) { "/assign @joe\nworld" } let(:final_msg) { "world" } end + + it 'allows slash in command arguments' do + msg = "/assign @joe / @jane\nworld" + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['assign', '@joe / @jane']] + expect(msg).to eq 'world' + end end context 'in the middle of content' do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb deleted file mode 100644 index 7a140518dd2..00000000000 --- a/spec/lib/gitlab/themes_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Themes, lib: true do - describe '.body_classes' do - it 'returns a space-separated list of class names' do - css = described_class.body_classes - - expect(css).to include('ui_graphite') - expect(css).to include(' ui_charcoal ') - expect(css).to include(' ui_blue') - end - end - - describe '.by_id' do - it 'returns a Theme by its ID' do - expect(described_class.by_id(1).name).to eq 'Graphite' - expect(described_class.by_id(6).name).to eq 'Blue' - end - end - - describe '.default' do - it 'returns the default application theme' do - allow(described_class).to receive(:default_id).and_return(2) - expect(described_class.default.id).to eq 2 - end - - it 'prevents an infinite loop when configuration default is invalid' do - default = described_class::APPLICATION_DEFAULT - themes = described_class::THEMES - - config = double(default_theme: 0).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - - config = double(default_theme: themes.size + 5).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - end - end - - describe '.each' do - it 'passes the block to the THEMES Array' do - ids = [] - described_class.each { |theme| ids << theme.id } - expect(ids).not_to be_empty - end - end -end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3f32248e52b..f8513ac8b1c 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -290,7 +290,7 @@ describe Ci::Runner, models: true do let!(:last_update) { runner.ensure_runner_queue_value } before do - runner.update(description: 'new runner') + Ci::UpdateRunnerService.new(runner).update(description: 'new runner') end it 'sets a new last_update value' do @@ -318,6 +318,25 @@ describe Ci::Runner, models: true do end end + describe '#destroy' do + let(:runner) { create(:ci_runner) } + + context 'when there is a tick in the queue' do + let!(:queue_key) { runner.send(:runner_queue_key) } + + before do + runner.tick_runner_queue + runner.destroy + end + + it 'cleans up the queue' do + Gitlab::Redis.with do |redis| + expect(redis.get(queue_key)).to be_nil + end + end + end + end + describe '.assignable_for' do let(:runner) { create(:ci_runner) } let(:project) { create(:empty_project) } diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 50ad5013df9..3bd7ec18ae0 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do 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(:duration) { 10 } let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do @@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } - let(:duration) { 10 } it 'returns a message with information about failed build' do expect(subject.pretext).to be_empty @@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do end end - def build_message(status_text = status) + 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 hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " 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_spec.rb b/spec/models/project_spec.rb index 35f3dd00870..b0087a9e15d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1852,8 +1852,8 @@ describe Project, models: true do end describe '#pages_url' do - let(:group) { create :group, name: group_name } - let(:project) { create :empty_project, namespace: group, name: project_name } + let(:group) { create :group, name: 'Group' } + let(:nested_group) { create :group, parent: group } let(:domain) { 'Example.com' } subject { project.pages_url } @@ -1863,18 +1863,37 @@ describe Project, models: true do allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') end - context 'group page' do - let(:group_name) { 'Group' } - let(:project_name) { 'group.example.com' } + context 'top-level group' do + let(:project) { create :empty_project, namespace: group, name: project_name } - it { is_expected.to eq("http://group.example.com") } + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com") } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com/project") } + end end - context 'project page' do - let(:group_name) { 'Group' } - let(:project_name) { 'Project' } + context 'nested group' do + let(:project) { create :empty_project, namespace: nested_group, name: project_name } + let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" } - it { is_expected.to eq("http://group.example.com/project") } + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq(expected_url) } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq(expected_url) } + end end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9bfa6409607..838fd3754b2 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -351,6 +351,17 @@ describe Repository, models: true do expect(blob.data).to eq('Changelog!') end + it 'respects the autocrlf setting' do + repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld", + message: 'Add hello world', + branch_name: 'master', + update: true) + + blob = repository.blob_at('master', 'hello.txt') + + expect(blob.data).to eq("Hello,\nWorld") + end + context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7fd49c73b37..584a4facd94 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -19,6 +19,7 @@ describe User, models: true do it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:destroy) } @@ -303,6 +304,34 @@ describe User, models: true do end end + shared_context 'user keys' do + let(:user) { create(:user) } + let!(:key) { create(:key, user: user) } + let!(:deploy_key) { create(:deploy_key, user: user) } + end + + describe '#keys' do + include_context 'user keys' + + context 'with key and deploy key stored' do + it 'returns stored key, but not deploy_key' do + expect(user.keys).to include key + expect(user.keys).not_to include deploy_key + end + end + end + + describe '#deploy_keys' do + include_context 'user keys' + + context 'with key and deploy key stored' do + it 'returns stored deploy key, but not normal key' do + expect(user.deploy_keys).to include deploy_key + expect(user.deploy_keys).not_to include key + end + end + end + describe '#confirm' do before do allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) @@ -553,18 +582,16 @@ describe User, models: true do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) - expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end describe 'with default overrides' do - let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) } + let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) } it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey - expect(user.theme_id).to eq(1) end end diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb new file mode 100644 index 00000000000..1caaa557085 --- /dev/null +++ b/spec/models/wiki_directory_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +RSpec.describe WikiDirectory, models: true do + describe 'validations' do + subject { build(:wiki_directory) } + + it { is_expected.to validate_presence_of(:slug) } + end + + describe '#initialize' do + context 'when there are pages' do + let(:pages) { [build(:wiki_page)] } + let(:directory) { WikiDirectory.new('/path_up_to/dir', pages) } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute' do + expect(directory.pages).to eq(pages) + end + end + + context 'when there are no pages' do + let(:directory) { WikiDirectory.new('/path_up_to/dir') } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute to an empty array' do + expect(directory.pages).to eq([]) + end + end + end + + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + directory = build(:wiki_directory) + + expect(directory.to_partial_path).to eq('projects/wikis/wiki_directory') + end + end +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 5c34b1b0a30..753dc938c52 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -7,6 +7,75 @@ describe WikiPage, models: true do subject { WikiPage.new(wiki) } + describe '.group_by_directory' do + context 'when there are no pages' do + it 'returns an empty array' do + expect(WikiPage.group_by_directory(nil)).to eq([]) + expect(WikiPage.group_by_directory([])).to eq([]) + end + end + + context 'when there are pages' do + before do + create_page('dir_1/dir_1_1/page_3', 'content') + create_page('dir_1/page_2', 'content') + create_page('dir_2/page_5', 'content') + create_page('dir_2/page_4', 'content') + create_page('page_1', 'content') + end + let(:page_1) { wiki.find_page('page_1') } + let(:dir_1) do + WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) + end + let(:dir_1_1) do + WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) + end + let(:dir_2) do + pages = [wiki.find_page('dir_2/page_5'), + wiki.find_page('dir_2/page_4')] + WikiDirectory.new('dir_2', pages) + end + + it 'returns an array with pages and directories' do + expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2] + + grouped_entries = WikiPage.group_by_directory(wiki.pages) + + grouped_entries.each_with_index do |page_or_dir, i| + expected_page_or_dir = expected_grouped_entries[i] + expected_slugs = get_slugs(expected_page_or_dir) + slugs = get_slugs(page_or_dir) + + expect(slugs).to match_array(expected_slugs) + end + end + + it 'returns an array sorted by alphabetical position' do + # Directories and pages within directories are sorted alphabetically. + # Pages at root come before everything. + expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3', + 'dir_2/page_4', 'dir_2/page_5'] + + grouped_entries = WikiPage.group_by_directory(wiki.pages) + + actual_order = + grouped_entries.map do |page_or_dir| + get_slugs(page_or_dir) + end. + flatten + expect(actual_order).to eq(expected_order) + end + end + end + + describe '.unhyphenize' do + it 'removes hyphens from a name' do + name = 'a-name--with-hyphens' + + expect(WikiPage.unhyphenize(name)).to eq('a name with hyphens') + end + end + describe "#initialize" do context "when initialized with an existing gollum page" do before do @@ -189,6 +258,26 @@ describe WikiPage, models: true do end end + describe '#directory' do + context 'when the page is at the root directory' do + it 'returns an empty string' do + create_page('file', 'content') + page = wiki.find_page('file') + + expect(page.directory).to eq('') + end + end + + context 'when the page is inside an actual directory' do + it 'returns the full directory hierarchy' do + create_page('dir_1/dir_1_1/file', 'content') + page = wiki.find_page('dir_1/dir_1_1/file') + + expect(page.directory).to eq('dir_1/dir_1_1') + end + end + end + describe '#historical?' do before do create_page('Update', 'content') @@ -221,6 +310,27 @@ describe WikiPage, models: true do end end + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + page = build(:wiki_page) + + expect(page.to_partial_path).to eq('projects/wikis/wiki_page') + end + end + + describe '#==' do + let(:original_wiki_page) { create(:wiki_page) } + + it 'returns true for identical wiki page' do + expect(original_wiki_page).to eq(original_wiki_page) + end + + it 'returns false for updated wiki page' do + updated_wiki_page = original_wiki_page.update("Updated content") + expect(original_wiki_page).not_to eq(updated_wiki_page) + end + end + private def remove_temp_repo(path) @@ -239,4 +349,12 @@ describe WikiPage, models: true do page = wiki.wiki.paged(title) wiki.wiki.delete_page(page, commit_details) end + + def get_slugs(page_or_dir) + if page_or_dir.is_a? WikiPage + [page_or_dir.slug] + else + page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : [] + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 5a3ffc284f2..3e66236f6ae 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -31,7 +31,18 @@ describe API::Branches, api: true do expect(response).to have_http_status(200) expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) + 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) diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index ee5865ad740..34abd72f455 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -159,6 +159,7 @@ describe API::CommitStatuses, api: true do context: 'coverage', ref: 'develop', description: 'test', + coverage: 80.0, target_url: 'http://gitlab.com/status' } post api(post_url, developer), optional_params @@ -170,6 +171,7 @@ describe API::CommitStatuses, api: true do expect(json_response['status']).to eq('success') expect(json_response['name']).to eq('coverage') expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(80.0) expect(json_response['description']).to eq('test') expect(json_response['target_url']).to eq('http://gitlab.com/status') end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index af9028a8978..3eef10c0698 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -367,11 +367,21 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project.repository.commit.id) - expect(json_response['title']).to eq(project.repository.commit.title) - expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) - expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) - expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) + commit = project.repository.commit + expect(json_response['id']).to eq(commit.id) + expect(json_response['short_id']).to eq(commit.short_id) + expect(json_response['title']).to eq(commit.title) + expect(json_response['message']).to eq(commit.safe_message) + expect(json_response['author_name']).to eq(commit.author_name) + expect(json_response['author_email']).to eq(commit.author_email) + expect(json_response['authored_date']).to eq(commit.authored_date.iso8601(3)) + expect(json_response['committer_name']).to eq(commit.committer_name) + expect(json_response['committer_email']).to eq(commit.committer_email) + expect(json_response['committed_date']).to eq(commit.committed_date.iso8601(3)) + expect(json_response['parent_ids']).to eq(commit.parent_ids) + expect(json_response['stats']['additions']).to eq(commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(commit.stats.deletions) + expect(json_response['stats']['total']).to eq(commit.stats.total) end it "returns a 404 error if not found" do @@ -464,6 +474,20 @@ describe API::Commits, api: true do expect(response).to have_http_status(401) end end + + context 'when the commit is present on two projects' do + let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) } + let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') } + + it 'returns the comments for the target project' do + get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq('a comment on a commit for fork') + expect(json_response.first['author']['id']).to eq(user2.id) + end + end end describe 'POST :id/repository/commits/:sha/cherry_pick' do diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb deleted file mode 100644 index 92ac4fd334d..00000000000 --- a/spec/requests/api/fork_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'spec_helper' - -describe API::Projects, api: true do - include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group) } - let(:group2) do - group = create(:group, name: 'group2_name') - group.add_owner(user2) - group - end - - describe 'POST /projects/fork/:id' do - let(:project) do - create(:project, :repository, creator: user, namespace: user.namespace) - end - - before do - project.add_reporter(user2) - end - - context 'when authenticated' do - it 'forks if user has sufficient access to project' do - post api("/projects/fork/#{project.id}", user2) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(user2.id) - expect(json_response['namespace']['id']).to eq(user2.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'forks if user is admin' do - post api("/projects/fork/#{project.id}", admin) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(admin.id) - expect(json_response['namespace']['id']).to eq(admin.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'fails on missing project access for the project to fork' do - new_user = create(:user) - post api("/projects/fork/#{project.id}", new_user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'fails if forked project exists in the user namespace' do - post api("/projects/fork/#{project.id}", user) - - expect(response).to have_http_status(409) - expect(json_response['message']['name']).to eq(['has already been taken']) - expect(json_response['message']['path']).to eq(['has already been taken']) - end - - it 'fails if project to fork from does not exist' do - post api('/projects/fork/424242', user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'forks with explicit own user namespace id' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks with explicit own user name as namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks to another user when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'fails if trying to fork to another user when not admin' do - post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id - - expect(response).to have_http_status(404) - end - - it 'fails if trying to fork to non-existent namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: 42424242 - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Target Namespace Not Found') - end - - it 'forks to owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group2.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group2.name) - end - - it 'fails to fork to not owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group.name - - expect(response).to have_http_status(404) - end - - it 'forks to not owned group when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: group.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group.name) - end - end - - context 'when unauthenticated' do - it 'returns authentication error' do - post api("/projects/fork/#{project.id}") - - expect(response).to have_http_status(401) - expect(json_response['message']).to eq('401 Unauthorized') - end - end - end -end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index f78bde6f53a..ccd7898586c 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -338,6 +338,26 @@ describe API::Groups, api: true do expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project3.name) end + + it 'only returns the projects owned by user' do + project2.group.add_owner(user3) + + get api("/groups/#{project2.group.id}/projects", user3), owned: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it 'only returns the projects starred by user' do + user1.starred_projects = [project1] + + get api("/groups/#{group1.id}/projects", user1), starred: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project1.name) + end end context "when authenticated as admin" do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 9892e014cb9..3e9bcfd1a60 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -145,11 +145,11 @@ describe API::Members, api: true do end end - it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + it "returns 409 if member already exists" do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: master.id, access_level: Member::MASTER - expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + expect(response).to have_http_status(409) end it 'returns 400 when user_id is not given' do diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 8beef821d6c..7bb208721a1 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -229,4 +229,40 @@ describe API::Milestones, api: true do end end end + + describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do + let(:merge_request) { create(:merge_request, source_project: project) } + before do + milestone.merge_requests << merge_request + end + + it 'returns project merge_requests for a particular milestone' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns a 404 error if milestone id not found' do + get api("/projects/#{project.id}/milestones/1234/merge_requests", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user) + + expect(response).to have_http_status(404) + end + + it 'returns a 401 error if user not authenticated' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests") + + expect(response).to have_http_status(401) + end + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index c1edf384d5c..a945d56f529 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -5,7 +5,7 @@ describe API::Namespaces, api: true do let(:admin) { create(:admin) } let(:user) { create(:user) } let!(:group1) { create(:group) } - let!(:group2) { create(:group) } + let!(:group2) { create(:group, :nested) } describe "GET /namespaces" do context "when unauthenticated" do @@ -25,11 +25,13 @@ describe API::Namespaces, api: true do end it "admin: returns an array of matched namespaces" do - get api("/namespaces?search=#{group1.name}", admin) + get api("/namespaces?search=#{group2.name}", admin) expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) + expect(json_response.last['path']).to eq(group2.path) + expect(json_response.last['full_path']).to eq(group2.full_path) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ac0bbec44e0..741815a780e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -41,26 +41,40 @@ describe API::Projects, api: true do end describe 'GET /projects' do - before { project } + shared_examples_for 'projects response' do + it 'returns an array of projects' do + get api('/projects', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + + let!(:public_project) { create(:empty_project, :public, name: 'public_project') } + before do + project + project2 + project3 + project4 + end context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects') - expect(response).to have_http_status(401) + it_behaves_like 'projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } end end context 'when authenticated as regular user' do - it 'returns an array of projects' do - get api('/projects', user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.name) - expect(json_response.first['owner']['username']).to eq(user.username) + it_behaves_like 'projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } end it 'includes the project labels as the tag_list' do get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array expect(json_response.first.keys).to include('tag_list') @@ -68,21 +82,39 @@ describe API::Projects, api: true do it 'includes open_issues_count' do get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array expect(json_response.first.keys).to include('open_issues_count') end - it 'does not include open_issues_count' do + it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array - expect(json_response.first.keys).not_to include('open_issues_count') + expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') + end + + it "does not include statistics by default" do + get api('/projects', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') end - context 'GET /projects?simple=true' do + it "includes statistics if requested" do + get api('/projects', user), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).to include 'statistics' + end + + context 'and with simple=true' do it 'returns a simplified version of all the projects' do expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] @@ -97,6 +129,7 @@ describe API::Projects, api: true do context 'and using search' do it 'returns searched project' do get api('/projects', user), { search: project.name } + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -106,196 +139,109 @@ describe API::Projects, api: true do context 'and using the visibility filter' do it 'filters based on private visibility param' do get api('/projects', user), { visibility: 'private' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id) end it 'filters based on internal visibility param' do + project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL) + get api('/projects', user), { visibility: 'internal' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id) end it 'filters based on public visibility param' do get api('/projects', user), { visibility: 'public' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) end end context 'and using sorting' do - before do - project2 - project3 - end - it 'returns the correct order when sorted by id' do get api('/projects', user), { order_by: 'id', sort: 'desc' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end end - end - end - - describe 'GET /projects/all' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/all') - expect(response).to have_http_status(401) - end - end - context 'when authenticated as regular user' do - it 'returns authentication error' do - get api('/projects/all', user) - expect(response).to have_http_status(403) - end - end + context 'and with owned=true' do + it 'returns an array of projects the user owns' do + get api('/projects', user4), owned: true - context 'when authenticated as admin' do - it 'returns an array of all projects' do - get api('/projects/all', admin) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry.has_key?('permissions') && - entry['name'] == project.name && - entry['owner']['username'] == user.username - end + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) end end - it "does not include statistics by default" do - get api('/projects/all', admin) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - get api('/projects/all', admin), statistics: true - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).to include 'statistics' - end - end - end - - describe 'GET /projects/owned' do - before do - project3 - project4 - end + context 'and with starred=true' do + let(:public_project) { create(:empty_project, :public) } - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/owned') - expect(response).to have_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'returns an array of projects the user owns' do - get api('/projects/owned', user4) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project4.name) - expect(json_response.first['owner']['username']).to eq(user4.username) - end - - it "does not include statistics by default" do - get api('/projects/owned', user4) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - commit_count: 23, - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345, - } - - project4.statistics.update!(attributes) + before do + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) + end - get api('/projects/owned', user4), statistics: true + it 'returns the starred projects viewable by the user' do + get api('/projects', user3), starred: true - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + end end - end - end - describe 'GET /projects/visible' do - shared_examples_for 'visible projects response' do - it 'returns the visible projects' do - get api('/projects/visible', current_user) + context 'and with all query parameters' do + # | | project5 | project6 | project7 | project8 | project9 | + # |---------+----------+----------+----------+----------+----------| + # | search | x | | x | x | x | + # | starred | x | x | | x | x | + # | public | x | x | x | | x | + # | owned | x | x | x | x | | + let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) } + let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) } + let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) } + let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) } + let!(:project9) { create(:empty_project, :public, path: 'gitlab9') } - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) - end - end - - let!(:public_project) { create(:empty_project, :public) } - before do - project - project2 - project3 - project4 - end + before do + user.update_attributes(starred_projects: [project5, project6, project8, project9]) + end - context 'when unauthenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { nil } - let(:projects) { [public_project] } - end - end + it 'returns only projects that satify all query parameters' do + get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } - context 'when authenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(project5.id) + end end end context 'when authenticated as a different user' do - it_behaves_like 'visible projects response' do + it_behaves_like 'projects response' do let(:current_user) { user2 } let(:projects) { [public_project] } end end - end - - describe 'GET /projects/starred' do - let(:public_project) { create(:empty_project, :public) } - - before do - project_member2 - user3.update_attributes(starred_projects: [project, project2, project3, public_project]) - end - it 'returns the starred projects viewable by the user' do - get api('/projects/starred', user3) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + context 'when authenticated as admin' do + it_behaves_like 'projects response' do + let(:current_user) { admin } + let(:projects) { Project.all } + end end end @@ -639,6 +585,7 @@ describe API::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end @@ -1332,4 +1279,130 @@ describe API::Projects, api: true do end end end + + describe 'POST /projects/:id/fork' do + let(:project) do + create(:project, :repository, creator: user, namespace: user.namespace) + end + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end + + before do + project.add_reporter(user2) + end + + context 'when authenticated' do + it 'forks if user has sufficient access to project' do + post api("/projects/#{project.id}/fork", user2) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'forks if user is admin' do + post api("/projects/#{project.id}/fork", admin) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(admin.id) + expect(json_response['namespace']['id']).to eq(admin.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'fails on missing project access for the project to fork' do + new_user = create(:user) + post api("/projects/#{project.id}/fork", new_user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'fails if forked project exists in the user namespace' do + post api("/projects/#{project.id}/fork", user) + + expect(response).to have_http_status(409) + expect(json_response['message']['name']).to eq(['has already been taken']) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'fails if project to fork from does not exist' do + post api('/projects/424242/fork', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'forks with explicit own user namespace id' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/#{project.id}/fork", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(404) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group.name + + expect(response).to have_http_status(404) + end + + it 'forks to not owned group when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/projects/#{project.id}/fork") + + expect(response).to have_http_status(401) + expect(json_response['message']).to eq('401 Unauthorized') + end + end + end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index f2d81a28cb8..daef9062e7d 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -183,6 +183,7 @@ describe API::Runners, api: true do it 'updates runner' do description = shared_runner.description active = shared_runner.active + runner_queue_value = shared_runner.ensure_runner_queue_value update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, @@ -197,18 +198,24 @@ describe API::Runners, api: true do expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.run_untagged?).to be(false) expect(shared_runner.locked?).to be(true) + expect(shared_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end context 'when runner is not shared' do it 'updates runner' do description = specific_runner.description + runner_queue_value = specific_runner.ensure_runner_queue_value + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response).to have_http_status(200) expect(specific_runner.description).to eq('test') expect(specific_runner.description).not_to eq(description) + expect(specific_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index d32ba60fc4c..c0a8c0832bb 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -3,23 +3,23 @@ require 'spec_helper' describe API::Templates, api: true do include ApiHelpers - shared_examples_for 'the Template Entity' do |path| - before { get api(path) } + context 'the Template Entity' do + before { get api('/templates/gitignores/Ruby') } it { expect(json_response['name']).to eq('Ruby') } it { expect(json_response['content']).to include('*.gem') } end - - shared_examples_for 'the TemplateList Entity' do |path| - before { get api(path) } + + context 'the TemplateList Entity' do + before { get api('/templates/gitignores') } it { expect(json_response.first['name']).not_to be_nil } it { expect(json_response.first['content']).to be_nil } end - shared_examples_for 'requesting gitignores' do |path| + context 'requesting gitignores' do it 'returns a list of available gitignore templates' do - get api(path) + get api('/templates/gitignores') expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -27,9 +27,9 @@ describe API::Templates, api: true do end end - shared_examples_for 'requesting gitlab-ci-ymls' do |path| + context 'requesting gitlab-ci-ymls' do it 'returns a list of available gitlab_ci_ymls' do - get api(path) + get api('/templates/gitlab_ci_ymls') expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -37,17 +37,17 @@ describe API::Templates, api: true do end end - shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| + context 'requesting gitlab-ci-yml for Ruby' do it 'adds a disclaimer on the top' do - get api(path) + get api('/templates/gitlab_ci_ymls/Ruby') expect(response).to have_http_status(200) expect(json_response['content']).to start_with("# This file is a template,") end end - shared_examples_for 'the License Template Entity' do |path| - before { get api(path) } + context 'the License Template Entity' do + before { get api('/templates/licenses/mit') } it 'returns a license template' do expect(json_response['key']).to eq('mit') @@ -64,9 +64,9 @@ describe API::Templates, api: true do end end - shared_examples_for 'GET licenses' do |path| + context 'GET templates/licenses' do it 'returns a list of available license templates' do - get api(path) + get api('/templates/licenses') expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -77,7 +77,7 @@ describe API::Templates, api: true do describe 'the popular parameter' do context 'with popular=1' do it 'returns a list of available popular license templates' do - get api("#{path}?popular=1") + get api('/templates/licenses?popular=1') expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -88,10 +88,10 @@ describe API::Templates, api: true do end end - shared_examples_for 'GET licenses/:name' do |path| + context 'GET templates/licenses/:name' do context 'with :project and :fullname given' do before do - get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") + get api("/templates/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") end context 'for the mit license' do @@ -178,26 +178,4 @@ describe API::Templates, api: true do end end end - - describe 'with /templates namespace' do - it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/templates/gitignores' - it_behaves_like 'requesting gitignores', '/templates/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/templates/licenses/mit' - it_behaves_like 'GET licenses', '/templates/licenses' - it_behaves_like 'GET licenses/:name', '/templates/licenses' - end - - describe 'without /templates namespace' do - it_behaves_like 'the Template Entity', '/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/gitignores' - it_behaves_like 'requesting gitignores', '/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/licenses/mit' - it_behaves_like 'GET licenses', '/licenses' - it_behaves_like 'GET licenses/:name', '/licenses' - end end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb new file mode 100644 index 00000000000..28c3ca03960 --- /dev/null +++ b/spec/requests/api/v3/members_spec.rb @@ -0,0 +1,342 @@ +require 'spec_helper' + +describe API::Members, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + end + end + + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + end + end + + shared_examples 'GET /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + end + end + + it 'does not return invitees' do + create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) + + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + + it 'finds members with query string' do + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username + + expect(response).to have_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(master.username) + end + end + end + + shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(200) + # User attributes + expect(json_response['id']).to eq(developer.id) + expect(json_response['name']).to eq(developer.name) + expect(json_response['username']).to eq(developer.username) + expect(json_response['state']).to eq(developer.state) + expect(json_response['avatar_url']).to eq(developer.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger), + user_id: access_requester.id, access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + post v3_api("/#{source_type.pluralize}/#{source.id}/members", user), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + context 'and new member is already a requester' do + it 'transforms the requester into a proper member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(source.requesters.count).to eq(0) + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'creates a new member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(json_response['id']).to eq(stranger.id) + expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: master.id, access_level: Member::MASTER + + expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + end + + it 'returns 400 when user_id is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + access_level: Member::MASTER + + expect(response).to have_http_status(400) + end + + it 'returns 400 when access_level is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access_level is not valid' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger), + access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user), + access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: Member::MASTER, expires_at: '2016-08-05' + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(developer.id) + expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it 'returns 409 if member does not exist' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master), + access_level: Member::MASTER + + expect(response).to have_http_status(404) + end + + it 'returns 400 when access_level is not given' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access level is not valid' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a member and deleting themself' do + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + context 'and member is a requester' do + it "returns #{source_type == 'project' ? 200 : 404}" do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end.not_to change { source.requesters.count } + end + end + + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end + end + end + + it_behaves_like 'GET /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + context 'Adding owner to project' do + it 'returns 403' do + expect do + post v3_api("/projects/#{project.id}/members", master), + user_id: stranger.id, access_level: Member::OWNER + + expect(response).to have_http_status(422) + end.to change { project.members.count }.by(0) + end + end +end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index a495122bba7..36d99d80e79 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -682,6 +682,7 @@ describe API::V3::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb new file mode 100644 index 00000000000..4fd4e70bedd --- /dev/null +++ b/spec/requests/api/v3/templates_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe API::V3::Templates, api: true do + include ApiHelpers + + shared_examples_for 'the Template Entity' do |path| + before { get v3_api(path) } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + shared_examples_for 'the TemplateList Entity' do |path| + before { get v3_api(path) } + + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end + + shared_examples_for 'requesting gitignores' do |path| + it 'returns a list of available gitignore templates' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end + + shared_examples_for 'requesting gitlab-ci-ymls' do |path| + it 'returns a list of available gitlab_ci_ymls' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).not_to be_nil + end + end + + shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| + it 'adds a disclaimer on the top' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response['content']).to start_with("# This file is a template,") + end + end + + shared_examples_for 'the License Template Entity' do |path| + before { get v3_api(path) } + + it 'returns a license template' do + expect(json_response['key']).to eq('mit') + expect(json_response['name']).to eq('MIT License') + expect(json_response['nickname']).to be_nil + expect(json_response['popular']).to be true + expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') + expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') + expect(json_response['description']).to include('A permissive license that is short and to the point.') + expect(json_response['conditions']).to eq(%w[include-copyright]) + expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) + expect(json_response['limitations']).to eq(%w[no-liability]) + expect(json_response['content']).to include('The MIT License (MIT)') + end + end + + shared_examples_for 'GET licenses' do |path| + it 'returns a list of available license templates' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(15) + expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') + end + + describe 'the popular parameter' do + context 'with popular=1' do + it 'returns a list of available popular license templates' do + get v3_api("#{path}?popular=1") + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.map { |l| l['key'] }).to include('apache-2.0') + end + end + end + end + + shared_examples_for 'GET licenses/:name' do |path| + context 'with :project and :fullname given' do + before do + get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") + end + + context 'for the mit license' do + let(:license_type) { 'mit' } + + it 'returns the license text' do + expect(json_response['content']).to include('The MIT License (MIT)') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") + end + end + + context 'for the agpl-3.0 license' do + let(:license_type) { 'agpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the gpl-3.0 license' do + let(:license_type) { 'gpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the gpl-2.0 license' do + let(:license_type) { 'gpl-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the apache-2.0 license' do + let(:license_type) { 'apache-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('Apache License') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") + end + end + + context 'for an uknown license' do + let(:license_type) { 'muth-over9000' } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'with no :fullname given' do + context 'with an authenticated user' do + let(:user) { create(:user) } + + it 'replaces the copyright owner placeholder with the name of the current user' do + get v3_api('/templates/licenses/mit', user) + + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") + end + end + end + end + + describe 'with /templates namespace' do + it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' + it_behaves_like 'the TemplateList Entity', '/templates/gitignores' + it_behaves_like 'requesting gitignores', '/templates/gitignores' + it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' + it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' + it_behaves_like 'the License Template Entity', '/templates/licenses/mit' + it_behaves_like 'GET licenses', '/templates/licenses' + it_behaves_like 'GET licenses/:name', '/templates/licenses' + end + + describe 'without /templates namespace' do + it_behaves_like 'the Template Entity', '/gitignores/Ruby' + it_behaves_like 'the TemplateList Entity', '/gitignores' + it_behaves_like 'requesting gitignores', '/gitignores' + it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' + it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' + it_behaves_like 'the License Template Entity', '/licenses/mit' + it_behaves_like 'GET licenses', '/licenses' + it_behaves_like 'GET licenses/:name', '/licenses' + end +end diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb new file mode 100644 index 00000000000..7cb24dc5646 --- /dev/null +++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key' + +describe RuboCop::Cop::Migration::AddConcurrentForeignKey do + include CopHelper + + let(:cop) { described_class.new } + + context 'outside of a migration' do + it 'does not register any offenses' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + expect(cop.offenses).to be_empty + end + end + + context 'in a migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when using add_foreign_key' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end +end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 1b95f1ff198..6a6df377b35 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -181,6 +181,17 @@ describe EnvironmentSerializer do expect(subject.first[:name]).to eq 'production' expect(subject.second[:name]).to eq 'staging' end + + it 'appends correct total page count header' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total', '3') + end + + it 'appends correct page count headers' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total-Pages', '2') + expect(response).to have_received(:[]=).with('X-Per-Page', '2') + end end end end diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb new file mode 100644 index 00000000000..e429fcfc72f --- /dev/null +++ b/spec/services/ci/update_runner_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Ci::UpdateRunnerService, :services do + let(:runner) { create(:ci_runner) } + + describe '#update' do + before do + allow(runner).to receive(:tick_runner_queue) + end + + context 'with description params' do + let(:params) { { description: 'new runner' } } + + it 'updates the runner and ticking the queue' do + expect(update).to be_truthy + + runner.reload + + expect(runner).to have_received(:tick_runner_queue) + expect(runner.description).to eq('new runner') + end + end + + context 'when params are not valid' do + let(:params) { { run_untagged: false } } + + it 'does not update and give false because it is not valid' do + expect(update).to be_falsey + + runner.reload + + expect(runner).not_to have_received(:tick_runner_queue) + expect(runner.run_untagged).to be_truthy + end + end + + def update + described_class.new(runner).update(params) + end + end +end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index f86189b68e9..32c2ed8cae7 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -9,14 +9,18 @@ describe Groups::DestroyService, services: true do let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } + before do + group.add_user(user, Gitlab::Access::OWNER) + end + shared_examples 'group destruction' do |async| context 'database records' do before do destroy_group(group, user, async) end - it { expect(Group.all).not_to include(group) } - it { expect(Project.all).not_to include(project) } + it { expect(Group.unscoped.all).not_to include(group) } + it { expect(Project.unscoped.all).not_to include(project) } end context 'file system' do @@ -32,7 +36,7 @@ describe Groups::DestroyService, services: true do context 'Sidekiq fake' do before do - # Dont run sidekiq to check if renamed repository exists + # Don't run sidekiq to check if renamed repository exists Sidekiq::Testing.fake! { destroy_group(group, user, async) } end @@ -95,4 +99,13 @@ describe Groups::DestroyService, services: true do describe 'synchronous delete' do it_behaves_like 'group destruction', false end + + context 'projects in pending_delete' do + before do + project.pending_delete = true + project.save + end + + it_behaves_like 'group destruction', false + end end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 4cfba35c830..09807e5d35b 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -120,11 +120,20 @@ describe Issues::BuildService, services: true do end describe '#execute' do + let(:milestone) { create(:milestone, project: project) } + it 'builds a new issues with given params' do - issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute + issue = described_class.new( + project, + user, + title: 'Issue #1', + description: 'Issue description', + milestone_id: milestone.id, + ).execute expect(issue.title).to eq('Issue #1') expect(issue.description).to eq('Issue description') + expect(issue.milestone).to eq(milestone) end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 30578ee4c7d..e1feeed8a67 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') + expect(issue.description).to eq('please fix') expect(issue.assignee).to be_nil expect(issue.labels).to be_empty expect(issue.milestone).to be_nil diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 5a89acc96a4..d96f819e66a 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do context "error handling" do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } - it 'saves error if there is an exception' do - allow(service).to receive(:repository).and_raise("error message") + before do + allow(Rails.logger).to receive(:error) + end + it 'logs and saves error if there is an exception' do + error_message = 'error message' + + allow(service).to receive(:repository).and_raise("error message") allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.merge_error).to eq("Something went wrong during merge: error message") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end - it 'saves error if there is an PreReceiveError exception' do - allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error") + it 'logs and saves error if there is an PreReceiveError exception' do + error_message = 'error message' + allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, error_message) allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.merge_error).to eq("error") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end - it 'aborts if there is a merge conflict' do + it 'logs and saves error if there is a merge conflict' do + error_message = 'Conflicts detected during merge' + allow_any_instance_of(Repository).to receive(:merge).and_return(false) allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.open?).to be_truthy + expect(merge_request).to be_open expect(merge_request.merge_commit_sha).to be_nil - expect(merge_request.merge_error).to eq("Conflicts detected during merge") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2cc21acab7b..983dac6efdb 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -287,41 +287,64 @@ describe MergeRequests::RefreshService, services: true do it 'references the commit that caused the Work in Progress status' do refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - allow(refresh_service).to receive(:find_new_commits) refresh_service.instance_variable_set("@commits", [ - instance_double( - Commit, + double( id: 'aaaaaaa', + sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e', short_id: 'aaaaaaa', title: 'Fix issue', work_in_progress?: false ), - instance_double( - Commit, + double( id: 'bbbbbbb', + sha: '498214de67004b1da3d820901307bed2a68a8ef6', short_id: 'bbbbbbb', title: 'fixup! Fix issue', work_in_progress?: true, to_reference: 'bbbbbbb' ), - instance_double( - Commit, + double( id: 'ccccccc', + sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141', short_id: 'ccccccc', title: 'fixup! Fix issue', work_in_progress?: true, to_reference: 'ccccccc' ), ]) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') reload_mrs - expect(@merge_request.notes.last.note).to eq( "marked as a **Work In Progress** from bbbbbbb" ) end + + it 'does not mark as WIP based on commits that do not belong to an MR' do + allow(refresh_service).to receive(:find_new_commits) + refresh_service.instance_variable_set("@commits", [ + double( + id: 'aaaaaaa', + sha: 'aaaaaaa', + short_id: 'aaaaaaa', + title: 'Fix issue', + work_in_progress?: false + ), + double( + id: 'bbbbbbb', + sha: 'bbbbbbbb', + short_id: 'bbbbbbb', + title: 'fixup! Fix issue', + work_in_progress?: true, + to_reference: 'bbbbbbb' + ) + ]) + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + + expect(@merge_request.work_in_progress?).to be_falsey + end end def reload_mrs diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 7cf2cd9968f..839250b7d84 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -146,6 +146,16 @@ 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) @@ -476,6 +486,20 @@ 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) @@ -665,6 +689,19 @@ 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) @@ -818,6 +855,20 @@ 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) @@ -1013,6 +1064,14 @@ describe NotificationService, services: true do should_not_email(@u_watcher) end + it "notifies the merger when merge_when_build_succeeds is false but they've opted into notifications about their activity" do + merge_request.merge_when_build_succeeds = false + @u_watcher.notified_of_own_activity = true + notification.merge_mr(merge_request, @u_watcher) + + should_email(@u_watcher) + end + it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/tags/create_service_spec.rb index 7dc43c50b0d..5478b8c9ec0 100644 --- a/spec/services/create_tag_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateTagService, services: true do +describe Tags::CreateService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index 477551f5036..a388c93379a 100644 --- a/spec/services/delete_tag_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DeleteTagService, services: true do +describe Tags::DestroyService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 13d584a8975..4320365ab57 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -9,7 +9,9 @@ describe TodoService, services: true do let(:admin) { create(:admin) } let(:john_doe) { create(:user) } let(:project) { create(:project) } - let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') } let(:service) { described_class.new } before do @@ -21,8 +23,10 @@ describe TodoService, services: true do describe 'Issues' do let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } + let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) } describe '#new_issue' do it 'creates a todo if assigned' do @@ -52,6 +56,26 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'creates a directly addressed todo for each valid addressed user' do + service.new_issue(addressed_issue, author) + + should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + + it 'creates correct todos for each valid user based on the type of mention' do + issue.update(description: directly_addressed_and_mentioned) + + service.new_issue(issue, author) + + should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) + end + it 'does not create todo if user can not see the issue when issue is confidential' do service.new_issue(confidential_issue, john_doe) @@ -63,6 +87,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + it 'does not create directly addressed todo if user cannot see the issue when issue is confidential' do + service.new_issue(addressed_confident_issue, john_doe) + + should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::ASSIGNED) + should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + end + context 'when a private group is mentioned' do let(:group) { create :group, :private } let(:project) { create :project, :private, group: group } @@ -94,12 +129,38 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'creates a todo for each valid user based on the type of mention' do + issue.update(description: directly_addressed_and_mentioned) + + service.update_issue(issue, author) + + should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) + should_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.update_issue(addressed_issue, author) + + should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not create a todo if user was already mentioned' do create(:todo, :mentioned, user: member, project: project, target: issue, author: author) expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) end + it 'does not create a directly addressed todo if user was already mentioned or addressed' do + create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author) + + expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count) + end + it 'does not create todo if user can not see the issue when issue is confidential' do service.update_issue(confidential_issue, john_doe) @@ -111,6 +172,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + it 'does not create a directly addressed todo if user can not see the issue when issue is confidential' do + service.update_issue(addressed_confident_issue, john_doe) + + should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + end + context 'issues with a task list' do it 'does not create todo when tasks are marked as completed' do issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") @@ -125,6 +197,19 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'does not create directly addressed todo when tasks are marked as completed' do + addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n") + + service.update_issue(addressed_issue, author) + + should_not_create_todo(user: admin, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: assignee, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not raise an error when description not change' do issue.update(title: 'Sample') @@ -244,8 +329,11 @@ describe TodoService, services: true do let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } + let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:addressed_note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: directly_addressed) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } + let(:addressed_note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: directly_addressed) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:system_note) { create(:system_note, project: project, noteable: issue) } @@ -276,6 +364,26 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end + it 'creates a todo for each valid user based on the type of mention' do + note.update(note: directly_addressed_and_mentioned) + + service.new_note(note, john_doe) + + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: note) + should_create_todo(user: admin, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.new_note(addressed_note, john_doe) + + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: author, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + end + it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do service.new_note(note_on_confidential_issue, john_doe) @@ -287,6 +395,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end + it 'does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue' do + service.new_note(addressed_note_on_confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + end + it 'creates a todo for each valid mentioned user when leaving a note on commit' do service.new_note(note_on_commit, john_doe) @@ -296,6 +415,15 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end + it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + end + it 'does not create todo when leaving a note on snippet' do should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) } end @@ -324,6 +452,7 @@ describe TodoService, services: true do describe 'Merge Requests' do let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } describe '#new_merge_request' do @@ -350,6 +479,25 @@ describe TodoService, services: true do should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end + + it 'creates a todo for each valid user based on the type of mention' do + mr_assigned.update(description: directly_addressed_and_mentioned) + + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.new_merge_request(addressed_mr_assigned, author) + + should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#update_merge_request' do @@ -363,12 +511,37 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end + it 'creates a todo for each valid user based on the type of mention' do + mr_assigned.update(description: directly_addressed_and_mentioned) + + service.update_merge_request(mr_assigned, author) + + should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.update_merge_request(addressed_mr_assigned, author) + + should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not create a todo if user was already mentioned' do create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author) expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end + it 'does not create a directly addressed todo if user was already mentioned or addressed' do + create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author) + + expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) + end + context 'with a task list' do it 'does not create todo when tasks are marked as completed' do mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") @@ -384,6 +557,20 @@ describe TodoService, services: true do should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + it 'does not create directly addressed todo when tasks are marked as completed' do + addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2") + + service.update_merge_request(addressed_mr_assigned, author) + + should_not_create_todo(user: admin, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: assignee, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not raise an error when description not change' do mr_assigned.update(title: 'Sample') @@ -436,6 +623,11 @@ describe TodoService, services: true do service.reassigned_merge_request(mr_assigned, author) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + + it 'does not create a directly addressed todo for guests' do + service.reassigned_merge_request(addressed_mr_assigned, author) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#merge_merge_request' do @@ -452,6 +644,11 @@ describe TodoService, services: true do service.merge_merge_request(mr_assigned, john_doe) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + + it 'does not create directly addressed todo for guests' do + service.merge_merge_request(addressed_mr_assigned, john_doe) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#new_award_emoji' do @@ -509,6 +706,7 @@ describe TodoService, services: true do describe '#new_note' do let(:mention) { john_doe.to_reference } let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") } let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } it 'creates a todo for mentioned user on new diff note' do @@ -517,6 +715,12 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request) end + it 'creates a directly addressed todo for addressed user on new diff note' do + service.new_note(addressed_diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::DIRECTLY_ADDRESSED, note: addressed_diff_note_on_merge_request) + end + it 'creates a todo for mentioned user on legacy diff note' do service.new_note(legacy_diff_note_on_merge_request, author) diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb new file mode 100644 index 00000000000..5341ba3d261 --- /dev/null +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe WikiPages::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:opts) do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute } + + it 'creates a valid wiki page' do + is_expected.to be_valid + expect(subject.title).to eq(opts[:title]) + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'create') + end + end + end +end diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb new file mode 100644 index 00000000000..a4b9a390fe2 --- /dev/null +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiPages::DestroyService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + it 'executes webhooks' do + service.execute(wiki_page) + + expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete') + end + end +end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb new file mode 100644 index 00000000000..2bccca764d7 --- /dev/null +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe WikiPages::UpdateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:opts) do + { + content: 'New content for wiki page', + format: 'markdown', + message: 'New wiki message' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute(wiki_page) } + + it 'updates the wiki page' do + is_expected.to be_valid + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + expect(subject.message).to eq(opts[:message]) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'update') + end + end + end +end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb new file mode 100644 index 00000000000..dac94dfc31e --- /dev/null +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -0,0 +1,35 @@ +shared_examples 'issuables list meta-data' do |issuable_type, action = nil| + before do + @issuable_ids = [] + + 2.times do + if issuable_type == :issue + issuable = create(issuable_type, project: project) + else + issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + @issuable_ids << issuable.id + + issuable.id.times { create(:note, noteable: issuable, project: issuable.project) } + (issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) } + (issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) } + end + end + + it "creates indexed meta-data object for issuable notes and votes count" do + if action + get action + else + get :index, namespace_id: project.namespace.path, project_id: project.path + end + + meta_data = assigns(:issuable_meta_data) + + @issuable_ids.each do |id| + expect(meta_data[id].notes_count).to eq(id) + expect(meta_data[id].downvotes).to eq(id + 1) + expect(meta_data[id].upvotes).to eq(id + 2) + end + end +end diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index dd54b0addda..c64574679b6 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do let(:example_params) do { assignee: create(:user), - milestone_id: double(:milestone), + milestone_id: 1, description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") } end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb new file mode 100644 index 00000000000..f5381a48207 --- /dev/null +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'projects/_home_panel', :view do + let(:project) { create(:empty_project, :public) } + + let(:notification_settings) do + user&.notification_settings_for(project) + end + + before do + assign(:project, project) + assign(:notification_setting, notification_settings) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:can?).and_return(false) + end + + context 'when user is signed in' do + let(:user) { create(:user) } + + it 'makes it possible to set notification level' do + render + + expect(view).to render_template('shared/notifications/_button') + expect(rendered).to have_selector('.notification-dropdown') + end + end + + context 'when user is signed out' do + let(:user) { nil } + + it 'is not possible to set notification level' do + render + + expect(rendered).not_to have_selector('.notification_dropdown') + end + end +end diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb index b14b1ece2d0..b61f016967f 100644 --- a/spec/views/projects/notes/_form.html.haml_spec.rb +++ b/spec/views/projects/notes/_form.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'projects/notes/_form' do let(:note) { build(:"note_on_#{noteable}", project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown and slash commands are supported') + expect(rendered).to have_content('Markdown and slash commands are supported') end end end @@ -30,7 +30,7 @@ describe 'projects/notes/_form' do let(:note) { build(:note_on_commit, project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown is supported') + expect(rendered).to have_content('Markdown is supported') end end end |