diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-04-03 17:22:18 +0100 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-04-03 17:22:18 +0100 |
commit | 1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce (patch) | |
tree | 7fd8628974752a279cbe27c61983f8755d92cc51 /spec | |
parent | 16cca3a0ea7f4b95e99d7b3e8d4953334fa7bec7 (diff) | |
parent | b2700e64cce7c9b258e117a995eda8de00a8a988 (diff) | |
download | gitlab-ce-1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce.tar.gz |
Merge branch 'master' into tc-fix-unplayable-build-action-404
* master: (525 commits)
Introduce "polling_interval_multiplier" as application setting
fix spelling CI_REPOSITORY_URL (line:355) gitab-ci-token to gitlab-ci-token.
Pass Gitaly Repository messages to workhorse
Use gitaly 0.5.0
Fix specs
Improve specs examples
Minor refactor
Fix BrachFormatter for removed users
Changelog
Fix specs
One more change to the branch names to preserve metadata
Prefixes source branch name with short SHA to avoid collision
Fix GitHub importer for PRs of deleted forked repositories
Change order of specs
Clean history after every test that changes history
Clean history state after each test
Fixes method not replacing URL parameters correctly
Fix a transient failure caused by FFaker
Remove unnecessary ORDER BY clause when updating todos
Add a wait_for_ajax call to ensure Todos page cleans up properly
...
Diffstat (limited to 'spec')
332 files changed, 6592 insertions, 1846 deletions
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb new file mode 100644 index 00000000000..5dd8f66343f --- /dev/null +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Admin::ApplicationSettingsController do + include StubENV + + let(:admin) { create(:admin) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + + describe 'PUT #update' do + before do + sign_in(admin) + end + + it 'updates the default_project_visibility for string value' do + put :update, application_setting: { default_project_visibility: "20" } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'update the restricted levels for string values' do + put :update, application_setting: { restricted_visibility_levels: %w[10 20] } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20]) + end + + it 'falls back to defaults when settings are omitted' do + put :update, application_setting: {} + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PRIVATE) + expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty + end + end +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index fa4cc0ebbe0..51f23e4eeb9 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -112,6 +112,17 @@ describe Import::BitbucketController do post :create, format: :js end end + + context 'when the Bitbucket user is unauthorized' do + render_views + + it 'returns unauthorized' do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:can?).and_return(false) + + post :create, format: :js + end + end end context "when the repository owner is not the Bitbucket user" do diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb index 18148acde3e..2f9d18e3a0e 100644 --- a/spec/controllers/profiles/accounts_controller_spec.rb +++ b/spec/controllers/profiles/accounts_controller_spec.rb @@ -1,25 +1,47 @@ require 'spec_helper' describe Profiles::AccountsController do - let(:user) { create(:omniauth_user, provider: 'saml') } + describe 'DELETE unlink' do + let(:user) { create(:omniauth_user) } - before do - sign_in(user) - end + before do + sign_in(user) + end - it 'does not allow to unlink SAML connected account' do - identity = user.identities.last - delete :unlink, provider: 'saml' - updated_user = User.find(user.id) + it 'renders 404 if someone tries to unlink a non existent provider' do + delete :unlink, provider: 'github' - expect(response).to have_http_status(302) - expect(updated_user.identities.size).to eq(1) - expect(updated_user.identities).to include(identity) - end + expect(response).to have_http_status(404) + end + + [:saml, :cas3].each do |provider| + describe "#{provider} provider" do + let(:user) { create(:omniauth_user, provider: provider.to_s) } + + it "does not allow to unlink connected account" do + identity = user.identities.last + + delete :unlink, provider: provider.to_s + + expect(response).to have_http_status(302) + expect(user.reload.identities).to include(identity) + end + end + end + + [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0].each do |provider| + describe "#{provider} provider" do + let(:user) { create(:omniauth_user, provider: provider.to_s) } + + it 'allows to unlink connected account' do + identity = user.identities.last - it 'does allow to delete other linked accounts' do - user.identities.create(provider: 'twitter', extern_uid: 'twitter_123') + delete :unlink, provider: provider.to_s - expect { delete :unlink, provider: 'twitter' }.to change(Identity.all, :size).by(-1) + expect(response).to have_http_status(302) + expect(user.reload.identities).not_to include(identity) + end + end + end end end diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb new file mode 100644 index 00000000000..b97cdd4d489 --- /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.notification_email = 'original@example.com' + user.save! + end + end + + describe 'GET show' do + it 'renders' do + sign_in(user) + + get :show + + expect(response).to render_template :show + end + end + + describe 'POST update' do + it 'updates only permitted attributes' do + sign_in(user) + + put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } + + user.reload + expect(user.notification_email).to eq('new@example.com') + expect(user.notified_of_own_activity).to eq(true) + expect(user.admin).to eq(false) + expect(controller).to set_flash[:notice].to('Notification settings saved') + end + + it 'shows an error message if the params are invalid' do + sign_in(user) + + put :update, user: { notification_email: '' } + + expect(user.reload.notification_email).to eq('original@example.com') + expect(controller).to set_flash[:alert].to('Failed to save new settings') + end + end +end diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb new file mode 100644 index 00000000000..683667129e5 --- /dev/null +++ b/spec/controllers/projects/builds_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::BuildsController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + describe 'GET status.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:status) { build.detailed_status(double('user')) } + + before do + get :status, namespace_id: project.namespace, + project_id: project, + id: build.id, + format: :json + end + + it 'return a detailed build status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end +end diff --git a/spec/controllers/projects/builds_controller_specs.rb b/spec/controllers/projects/builds_controller_specs.rb new file mode 100644 index 00000000000..d501f7b3155 --- /dev/null +++ b/spec/controllers/projects/builds_controller_specs.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Projects::BuildsController do + include ApiHelpers + + let(:project) { create(:empty_project, :public) } + + describe 'GET trace.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:user) { create(:user) } + + context 'when user is logged in as developer' do + before do + project.add_developer(user) + sign_in(user) + get_trace + end + + it 'traces build log' do + expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status + end + end + + context 'when user is logged in as non member' do + before do + sign_in(user) + get_trace + end + + it 'traces build log' do + expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status + end + end + + def get_trace + get :trace, namespace_id: project.namespace, + project_id: project, + id: build.id, + format: :json + end + end +end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 83d80b376fb..5525fbd8130 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -81,6 +81,39 @@ describe Projects::EnvironmentsController do end end + describe 'GET folder' do + before do + create(:environment, project: project, + name: 'staging-1.0/review', + state: :available) + end + + context 'when using default format' do + it 'responds with HTML' do + get :folder, namespace_id: project.namespace, + project_id: project, + id: 'staging-1.0' + + expect(response).to be_ok + expect(response).to render_template 'folder' + end + end + + context 'when using JSON format' do + it 'responds with JSON' do + get :folder, namespace_id: project.namespace, + project_id: project, + id: 'staging-1.0', + format: :json + + expect(response).to be_ok + expect(response).not_to render_template 'folder' + expect(json_response['environments'][0]) + .to include('name' => 'staging-1.0/review') + end + end + end + describe 'GET show' do context 'with valid id' do it 'responds with a status code 200' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 6ceaf96f78f..734966d50b2 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -87,6 +87,13 @@ describe Projects::IssuesController do end describe 'GET #new' do + it 'redirects to signin if not logged in' do + get :new, namespace_id: project.namespace, project_id: project + + expect(flash[:notice]).to eq 'Please sign in to create the new issue.' + expect(response).to redirect_to(new_user_session_path) + end + context 'internal issue tracker' do before do sign_in(user) @@ -121,6 +128,11 @@ describe Projects::IssuesController do end context 'external issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end + it 'redirects to the external issue tracker' do external = double(new_issue_path: 'https://example.com/issues/new') allow(project).to receive(:external_issue_tracker).and_return(external) @@ -141,6 +153,24 @@ describe Projects::IssuesController do it_behaves_like 'update invalid issuable', Issue + context 'changing the assignee' do + it 'limits the attributes exposed on the assignee' do + assignee = create(:user) + project.add_developer(assignee) + + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: issue.iid, + issue: { assignee_id: assignee.id }, + format: :json + body = JSON.parse(response.body) + + expect(body['assignee'].keys) + .to match_array(%w(name username avatar_url)) + end + end + context 'when moving issue to another private project' do let(:another_project) { create(:empty_project, :private) } @@ -212,10 +242,27 @@ describe Projects::IssuesController do expect(spam_logs.first.recaptcha_verified).to be_falsey end - it 'renders verify template' do - update_spam_issue + context 'as HTML' do + it 'renders verify template' do + update_spam_issue + + expect(response).to render_template(:verify) + end + end + + context 'as JSON' do + before do + update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json) + end + + it 'renders json errors' do + expect(json_response) + .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) + end - expect(response).to render_template(:verify) + it 'returns 422 status' do + expect(response).to have_http_status(422) + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 250d64f7055..72f41f7209a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -203,6 +203,24 @@ describe Projects::MergeRequestsController do end describe 'PUT update' do + context 'changing the assignee' do + it 'limits the attributes exposed on the assignee' do + assignee = create(:user) + project.add_developer(assignee) + + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + merge_request: { assignee_id: assignee.id }, + format: :json + body = JSON.parse(response.body) + + expect(body['assignee'].keys) + .to match_array(%w(name username avatar_url)) + end + end + context 'there is no source project' do let(:project) { create(:project) } let(:fork_project) { create(:forked_project_with_submodules) } @@ -1160,4 +1178,42 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET pipeline_status.json' do + context 'when head_pipeline exists' do + let!(:pipeline) do + create(:ci_pipeline, project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + let(:status) { pipeline.detailed_status(double('user')) } + + before { get_pipeline_status } + + it 'return a detailed head_pipeline status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end + + context 'when head_pipeline does not exist' do + before { get_pipeline_status } + + it 'return empty' do + expect(response).to have_http_status(:ok) + expect(json_response).to be_empty + end + end + + def get_pipeline_status + get :pipeline_status, namespace_id: project.namespace, + project_id: project, + id: merge_request.iid, + format: :json + end + end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 04bb5cbbd59..d8f9bfd0d37 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -69,4 +69,24 @@ describe Projects::PipelinesController do format: :json end end + + describe 'GET status.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:status) { pipeline.detailed_status(double('user')) } + + before do + get :status, namespace_id: project.namespace, + project_id: project, + id: pipeline.id, + format: :json + end + + it 'return a detailed pipeline status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index e3f3b4fe8eb..1ecfe48475c 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -35,7 +35,7 @@ describe Projects::VariablesController do context 'updating a variable with valid characters' do before do - variable.gl_project_id = project.id + variable.project_id = project.id project.variables << variable end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 8cc216445eb..71dd9ef3eb4 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -30,6 +30,15 @@ describe RegistrationsController do expect(subject.current_user).to be_nil end end + + context 'when signup_enabled? is false' do + it 'redirects to sign_in' do + allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false) + + expect { post(:create, user_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + end + end end context 'when reCAPTCHA is enabled' do @@ -59,4 +68,20 @@ describe RegistrationsController do end end end + + describe '#destroy' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'schedules the user for destruction' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id) + + post(:destroy) + + expect(response.status).to eq(302) + end + end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index a06c29dd91a..9c16a7bc08b 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -211,4 +211,20 @@ describe SessionsController do end end end + + describe '#new' do + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + + it 'redirects correctly for referer on same host with params' do + search_path = '/search?search=seed_project' + allow(controller.request).to receive(:referer). + and_return('http://%{host}%{path}' % { host: Gitlab.config.gitlab.host, path: search_path }) + + get(:new, redirect_to_referer: :yes) + + expect(controller.stored_location_for(:redirect)).to eq(search_path) + end + end end diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index a581725245a..4df9aef2846 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -3,7 +3,7 @@ FactoryGirl.define do project factory: :empty_project after(:create) do |board| - board.lists.create(list_type: :done) + board.lists.create(list_type: :closed) end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6b0d084614b..f78086211f7 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -172,7 +172,7 @@ FactoryGirl.define do { image: 'ruby:2.1', services: ['postgres'], - after_script: "ls\ndate", + after_script: %w(ls date), artifacts: { name: 'artifacts_file', untracked: false, diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb index 3372e5ab685..6712dd5d82e 100644 --- a/spec/factories/ci/runner_projects.rb +++ b/spec/factories/ci/runner_projects.rb @@ -1,6 +1,6 @@ FactoryGirl.define do factory :ci_runner_project, class: Ci::RunnerProject do runner_id 1 - gl_project_id 1 + project_id 1 end end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index 2a2f3cca91c..f6a78811cbe 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,8 +6,8 @@ FactoryGirl.define do sequence(:position) end - factory :done_list, parent: :list do - list_type :done + factory :closed_list, parent: :list do + list_type :closed label nil position nil end diff --git a/spec/factories/system_note_metadata.rb b/spec/factories/system_note_metadata.rb new file mode 100644 index 00000000000..f487a2d7e4a --- /dev/null +++ b/spec/factories/system_note_metadata.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :system_note_metadata do + note + action 'merge' + end +end diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb index bc957ec72e1..d6c63f66a9b 100644 --- a/spec/features/admin/admin_broadcast_messages_spec.rb +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -45,7 +45,7 @@ feature 'Admin Broadcast Messages', feature: true do page.within('.broadcast-message-preview') do expect(page).to have_selector('strong', text: 'Markdown') - expect(page).to have_selector('img.emoji') + expect(page).to have_selector('gl-emoji[data-name="tada"]') end end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index de42ab81fac..5099441dce2 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do visit admin_application_settings_path end + scenario 'Change visibility settings' do + choose "application_setting_default_project_visibility_20" + click_button 'Save' + + expect(page).to have_content "Application settings saved successfully" + end + scenario 'Change application settings' do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' @@ -26,7 +33,8 @@ feature 'Admin updates settings', feature: true do fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' - page.check('Notify only broken builds') + page.check('Notify only broken pipelines') + page.check('Notify only default branch') check_all_events click_on 'Save' @@ -50,7 +58,6 @@ feature 'Admin updates settings', feature: true do page.check('Note') page.check('Issue') page.check('Merge request') - page.check('Build') page.check('Pipeline') end end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index a7c22615b89..58b14e09740 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe "Dashboard Issues Feed", feature: true do describe "GET /issues" do - let!(:user) { create(:user) } + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:project1) { create(:project) } let!(:project2) { create(:project) } @@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do end context "issue with basic fields" do - let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') } + let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') } it "renders issue fields" do visit issues_dashboard_path(:atom, private_token: user.private_token) @@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true do entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") expect(entry).to be_present - expect(entry).to have_selector('author email', text: issue2.author_email) - expect(entry).to have_selector('assignee email', text: issue2.author_email) + expect(entry).to have_selector('author email', text: issue2.author_public_email) + expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email) expect(entry).not_to have_selector('labels') expect(entry).not_to have_selector('milestone') expect(entry).to have_selector('description', text: issue2.description) @@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do context "issue with label and milestone" do let!(:milestone1) { create(:milestone, project: project1, title: 'v1') } let!(:label1) { create(:label, project: project1, title: 'label1') } - let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) } + let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) } before do issue1.labels << label1 @@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true do entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") expect(entry).to be_present - expect(entry).to have_selector('author email', text: issue1.author_email) - expect(entry).to have_selector('assignee email', text: issue1.author_email) + expect(entry).to have_selector('author email', text: issue1.author_public_email) + expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email) expect(entry).to have_selector('labels label', text: label1.title) expect(entry).to have_selector('milestone', text: milestone1.title) expect(entry).not_to have_selector('description') diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index a01a050a013..b3903ec2faf 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe 'Issues Feed', feature: true do describe 'GET /issues' do - let!(:user) { create(:user) } + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:group) { create(:group) } let!(:project) { create(:project) } - let!(:issue) { create(:issue, author: user, project: project) } + let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) } before do project.team << [user, :developer] @@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true do expect(response_headers['Content-Type']). to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") - expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('author email', text: issue.author_public_email) + expect(body).to have_selector('assignee email', text: issue.author_public_email) expect(body).to have_selector('entry summary', text: issue.title) end end @@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true do expect(response_headers['Content-Type']). to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") - expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('author email', text: issue.author_public_email) + expect(body).to have_selector('assignee email', text: issue.author_public_email) expect(body).to have_selector('entry summary', text: issue.title) end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index d17a418b8c3..1c0f97d8a1c 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -23,6 +23,20 @@ describe 'Issue Boards add issue modal', :feature, :js do wait_for_vue_resource end + it 'resets filtered search state' do + visit namespace_project_board_path(project.namespace, project, board, search: 'testing') + + wait_for_vue_resource + + click_button('Add issues') + + page.within('.add-issues-modal') do + expect(find('.form-control').value).to eq('') + expect(page).to have_selector('.clear-search', visible: false) + expect(find('.form-control')[:placeholder]).to eq('Search or filter results...') + end + end + context 'modal interaction' do it 'opens modal' do click_button('Add issues') diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index f7e8b78b54d..e168585534d 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -42,7 +42,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'creates default lists' do - lists = ['To Do', 'Doing', 'Done'] + lists = ['To Do', 'Doing', 'Closed'] page.within(find('.board-blank-state')) do click_button('Add default lists') @@ -65,7 +65,7 @@ describe 'Issue Boards', feature: true, js: true do let(:testing) { create(:label, project: project, name: 'Testing') } let(:bug) { create(:label, project: project, name: 'Bug') } let!(:backlog) { create(:label, project: project, name: 'Backlog') } - let!(:done) { create(:label, project: project, name: 'Done') } + let!(:closed) { create(:label, project: project, name: 'Closed') } let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } let!(:list1) { create(:list, board: board, label: planning, position: 0) } @@ -114,7 +114,7 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'search done list' do + it 'search closed list' do find('.filtered-search').set(issue8.title) find('.filtered-search').native.send_keys(:enter) @@ -186,13 +186,13 @@ describe 'Issue Boards', feature: true, js: true do end end - context 'done' do - it 'shows list of done issues' do + context 'closed' do + it 'shows list of closed issues' do wait_for_board_cards(3, 1) wait_for_ajax end - it 'moves issue to done' do + it 'moves issue to closed' do drag(list_from_index: 0, list_to_index: 2) wait_for_board_cards(1, 7) @@ -205,7 +205,7 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.board:nth-child(3)')).not_to have_content(planning.title) end - it 'removes all of the same issue to done' do + it 'removes all of the same issue to closed' do drag(list_from_index: 0, list_to_index: 2) wait_for_board_cards(1, 7) @@ -252,7 +252,7 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) end - it 'issue moves from done' do + it 'issue moves from closed' do drag(list_from_index: 2, list_to_index: 1) expect(find('.board:nth-child(2)')).to have_content(issue8.title) @@ -308,12 +308,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.board', count: 4) end - it 'creates new list for Done label' do + it 'creates new list for Closed label' do click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do - click_link done.title + click_link closed.title end wait_for_vue_resource @@ -326,7 +326,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax page.within('.dropdown-menu-issues-board-new') do - click_link done.title + click_link closed.title end wait_for_vue_resource diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 6d14a8cf483..e6d7cf106d4 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -25,7 +25,7 @@ describe 'Issue Boards new issue', feature: true, js: true do expect(page).to have_selector('.board-issue-count-holder .btn', count: 1) end - it 'does not display new issue button in done list' do + it 'does not display new issue button in closed list' do page.within('.board:nth-child(2)') do expect(page).not_to have_selector('.board-issue-count-holder .btn') end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 0e305c52358..881f1fca4d1 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -11,12 +11,16 @@ describe 'Commits' do stub_ci_pipeline_to_return_yaml_file end + let(:creator) { create(:user) } + let!(:pipeline) do create(:ci_pipeline, project: project, + user: creator, ref: project.default_branch, sha: project.commit.sha, - status: :success) + status: :success, + created_at: 5.months.ago) end context 'commit status is Generic Commit Status' do @@ -80,7 +84,8 @@ describe 'Commits' do it 'shows pipeline`s data' do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name + expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y') end end @@ -150,7 +155,7 @@ describe 'Commits' do it do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name expect(page).to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry') @@ -169,7 +174,7 @@ describe 'Commits' do it do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name expect(page).not_to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry') diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 62a2c54c94c..3642c0bfb5b 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -21,6 +21,11 @@ feature 'Dashboard shortcuts', feature: true, js: true do find('body').native.send_key('m') check_page_title('Merge Requests') + + find('body').native.send_key('g') + find('body').native.send_key('t') + + check_page_title('Todos') end def check_page_title(title) diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 773ae4b38bc..9daaaa8e555 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -7,6 +7,7 @@ describe 'Explore Groups page', js: true, feature: true do let!(:group) { create(:group) } let!(:public_group) { create(:group, :public) } let!(:private_group) { create(:group, :private) } + let!(:empty_project) { create(:empty_project, group: public_group) } before do group.add_owner(user) @@ -43,4 +44,23 @@ describe 'Explore Groups page', js: true, feature: true do expect(page).not_to have_content(private_group.full_name) expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 end + + it 'shows non-archived projects count' do + # Initially project is not archived + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + + # Archive project + empty_project.archive! + visit explore_groups_path + + # Check project count + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0") + + # Unarchive project + empty_project.unarchive! + visit explore_groups_path + + # Check project count + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + end end diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle.rb deleted file mode 100644 index ada4ac66e04..00000000000 --- a/spec/features/groups/group_name_toggle.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -feature 'Group name toggle', js: true do - let(:group) { create(:group) } - let(:nested_group_1) { create(:group, parent: group) } - let(:nested_group_2) { create(:group, parent: nested_group_1) } - let(:nested_group_3) { create(:group, parent: nested_group_2) } - - before do - login_as :user - end - - it 'is not present for less than 3 groups' do - visit group_path(group) - expect(page).not_to have_css('.group-name-toggle') - - visit group_path(nested_group_1) - expect(page).not_to have_css('.group-name-toggle') - end - - it 'is present for nested group of 3 or more in the namespace' do - visit group_path(nested_group_2) - expect(page).to have_css('.group-name-toggle') - - visit group_path(nested_group_3) - expect(page).to have_css('.group-name-toggle') - end - - context 'for group with at least 3 groups' do - before do - visit group_path(nested_group_2) - end - - it 'should show the full group namespace when toggled' do - expect(page).not_to have_content(group.name) - expect(page).to have_css('.group-path.hidable', visible: false) - - click_button '...' - - expect(page).to have_content(group.name) - expect(page).to have_css('.group-path.hidable', visible: true) - end - end -end diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb new file mode 100644 index 00000000000..8a1d415c4f1 --- /dev/null +++ b/spec/features/groups/group_name_toggle_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Group name toggle', feature: true, js: true do + let(:group) { create(:group) } + let(:nested_group_1) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group_1) } + let(:nested_group_3) { create(:group, parent: nested_group_2) } + + SMALL_SCREEN = 300 + + before do + login_as :user + end + + it 'is not present if enough horizontal space' do + visit group_path(nested_group_3) + + container_width = page.evaluate_script("$('.title-container')[0].offsetWidth") + title_width = page.evaluate_script("$('.title')[0].offsetWidth") + + expect(container_width).to be > title_width + expect(page).not_to have_css('.group-name-toggle') + end + + it 'is present if the title is longer than the container' do + visit group_path(nested_group_3) + title_width = page.evaluate_script("$('.title')[0].offsetWidth") + + page_height = page.current_window.size[1] + page.current_window.resize_to(SMALL_SCREEN, page_height) + + find('.group-name-toggle') + container_width = page.evaluate_script("$('.title-container')[0].offsetWidth") + + expect(title_width).to be > container_width + end + + it 'should show the full group namespace when toggled' do + page_height = page.current_window.size[1] + page.current_window.resize_to(SMALL_SCREEN, page_height) + visit group_path(nested_group_3) + + expect(page).not_to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: false) + + click_button '...' + + expect(page).to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: true) + end +end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index d243f9478bb..c90cc06a8f5 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -46,7 +46,7 @@ feature 'Group', feature: true do describe 'Mattermost team creation' do before do - allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled) + stub_mattermost_setting(enabled: mattermost_enabled) visit new_group_path end @@ -100,6 +100,16 @@ feature 'Group', feature: true do end end + it 'checks permissions to avoid exposing groups by parent_id' do + group = create(:group, :private, path: 'secret-group') + + logout + login_as(:user) + visit new_group_path(parent_id: group.id) + + expect(page).not_to have_content('secret-group') + end + describe 'group edit' do let(:group) { create(:group) } let(:path) { edit_group_path(group) } diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 01b657bcada..bc8cbe30e66 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -43,10 +43,10 @@ describe 'Dropdown hint', js: true, feature: true do end describe 'filtering' do - it 'does not filter `Keep typing and press Enter`' do + it 'does not filter `Press Enter or click to search`' do filtered_search.set('randomtext') - expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(page).to have_css(js_dropdown_hint, text: 'Press Enter or click to search', visible: false) expect(dropdown_hint_size).to eq(0) end diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 41ff31d2b99..3fde85b0a5c 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -17,13 +17,13 @@ feature 'Manually create a todo item from issue', feature: true, js: true do expect(page).to have_content 'Mark done' end - page.within '.header-content .todos-pending-count' do + page.within '.header-content .todos-count' do expect(page).to have_content '1' end visit namespace_project_issue_path(project.namespace, project, issue) - page.within '.header-content .todos-pending-count' do + page.within '.header-content .todos-count' do expect(page).to have_content '1' end end @@ -34,10 +34,10 @@ feature 'Manually create a todo item from issue', feature: true, js: true do click_button 'Mark done' end - expect(page).to have_selector('.todos-pending-count', visible: false) + expect(page).to have_selector('.todos-count', visible: false) visit namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_selector('.todos-pending-count', visible: false) + expect(page).to have_selector('.todos-count', visible: false) end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 1c8267b1593..7afceb88cf9 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -6,13 +6,20 @@ describe 'Issues', feature: true do include SortingHelper include WaitForAjax - let(:project) { create(:project) } + let(:project) { create(:project, :public) } before do login_as :user user2 = create(:user) project.team << [[@user, user2], :developer] + + project.repository.create_file( + @user, + '.gitlab/issue_templates/bug.md', + 'this is a test "bug" template', + message: 'added issue template', + branch_name: 'master') end describe 'Edit issue' do @@ -565,6 +572,24 @@ describe 'Issues', feature: true do end describe 'new issue' do + context 'by unauthenticated user' do + before do + logout + end + + it 'redirects to signin then back to new issue after signin' do + visit namespace_project_issues_path(project.namespace, project) + + click_link 'New issue' + + expect(current_path).to eq new_user_session_path + + login_as :user + + expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project) + end + end + context 'dropzone upload file', js: true do before do visit new_namespace_project_issue_path(project.namespace, project) @@ -582,6 +607,16 @@ describe 'Issues', feature: true do expect(page.find_field("issue_description").value).to match /\n\n$/ end end + + context 'form filled by URL parameters' do + before do + visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug') + end + + it 'fills in template' do + expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug') + end + end end describe 'new issue by email' do diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 0832a3656a8..f1ad4a55246 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -12,6 +12,33 @@ feature 'Create New Merge Request', feature: true, js: true do login_as user end + it 'selects the source branch sha when a tag with the same name exists' do + visit namespace_project_merge_requests_path(project.namespace, project) + + click_link 'New Merge Request' + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'v1.1.0').click + + expect(page).to have_content "b83d6e3" + end + + it 'selects the target branch sha when a tag with the same name exists' do + visit namespace_project_merge_requests_path(project.namespace, project) + + click_link 'New Merge Request' + + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + + first('.js-target-branch').click + first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click + + expect(page).to have_content "b83d6e3" + end + it 'generates a diff for an orphaned branch' do visit namespace_project_merge_requests_path(project.namespace, project) @@ -46,6 +73,12 @@ feature 'Create New Merge Request', feature: true, js: true do end end + it 'populates source branch button' do + visit new_namespace_project_merge_request_path(project.namespace, project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) + + expect(find('.js-source-branch')).to have_content('fix') + end + it 'allows to change the diff view' do visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 73c5ef31edc..18833ba7266 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -60,9 +60,6 @@ feature 'Merge request created from fork' do expect(page).to have_content pipeline.status expect(page).to have_content pipeline.id end - - expect(page.find('a.btn-remove')[:href]) - .to include fork_project.path_with_namespace end end diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index d5e3d8e7eff..69164aabdb2 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -296,7 +296,7 @@ feature 'Diff notes resolve', feature: true, js: true do it 'displays next discussion even if hidden' do page.all('.note-discussion').each do |discussion| page.within discussion do - click_link 'Toggle discussion' + click_button 'Toggle discussion' end end @@ -477,13 +477,13 @@ feature 'Diff notes resolve', feature: true, js: true do it 'shows resolved icon' do expect(page).to have_content '1/1 discussion resolved' - click_link 'Toggle discussion' + click_button 'Toggle discussion' expect(page).to have_selector('.line-resolve-btn.is-active') end it 'does not allow user to click resolve button' do expect(page).to have_selector('.line-resolve-btn.is-disabled') - click_link 'Toggle discussion' + click_button 'Toggle discussion' expect(page).to have_selector('.line-resolve-btn.is-disabled') end diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb index 3dbe26cddb0..1bc2a5548dd 100644 --- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -41,7 +41,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do visit namespace_project_merge_request_path(project.namespace, project, merge_request) expect(textbox).not_to be_visible - click_link "Modify commit message" + click_button "Modify commit message" expect(textbox).to be_visible end diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index 0ceaf7bc830..79105b1ee46 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -35,6 +35,8 @@ feature 'Merge immediately', :feature, :js do click_link 'Merge Immediately' expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress') + + wait_for_ajax end end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 6fed1568fcf..14511707af4 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -49,6 +49,26 @@ feature 'Merge requests filter clear button', feature: true, js: true do end end + context 'when multiple label filters have been applied' do + let!(:label) { create(:label, project: project, name: 'Frontend') } + let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") } + + before do + visit_merge_requests(project) + init_label_search + end + + it 'filters bug label' do + filtered_search.set('~bug') + + filter_dropdown.find('.filter-dropdown-item', text: bug.title).click + init_label_search + + expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible + expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + end + end + context 'when a text search has been conducted' do it 'resets the text search filter' do visit_merge_requests(project, search: 'Bug') diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index a2cf9b18bf2..3acd3f6a8b3 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -18,7 +18,7 @@ feature 'toggler_behavior', js: true, feature: true do it 'should be scrolled down to fragment' do page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") + fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)") expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true expect(fragment_position_top).to be >= page_scroll_y diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 2f3c3e45ae6..a1f4eb2688b 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it 'changes target_branch in new merge_request' do visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) - click_button "Compare branches and continue" fill_in "merge_request_title", with: 'My brand new feature' fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index c2545b0c259..decad589c23 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -1,101 +1,62 @@ require 'spec_helper' -feature 'Member autocomplete', feature: true do - include WaitForAjax - +feature 'Member autocomplete', :js do let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:participant) { create(:user) } let(:author) { create(:user) } + let(:note) { create(:note, noteable: noteable, project: noteable.project) } before do - allow_any_instance_of(Commit).to receive(:author).and_return(author) - login_as user + note # actually create the note + login_as(user) end - shared_examples "open suggestions" do - it 'displays suggestions' do - expect(page).to have_selector('.atwho-view', visible: true) - end - - it 'suggests author' do - page.within('.atwho-view', visible: true) do - expect(page).to have_content(author.username) + shared_examples "open suggestions when typing @" do + before do + page.within('.new-note') do + find('#note_note').send_keys('@') end end - it 'suggests participant' do + it 'suggests noteable author and note author' do page.within('.atwho-view', visible: true) do - expect(page).to have_content(participant.username) + expect(page).to have_content(author.username) + expect(page).to have_content(note.author.username) end end end - context 'adding a new note on a Issue', js: true do + context 'adding a new note on a Issue' do + let(:noteable) { create(:issue, author: author, project: project) } before do - issue = create(:issue, author: author, project: project) - create(:note, note: 'Ultralight Beam', noteable: issue, - project: project, author: participant) - visit_issue(project, issue) + visit namespace_project_issue_path(project.namespace, project, noteable) end - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end + include_examples "open suggestions when typing @" end - context 'adding a new note on a Merge Request ', js: true do + context 'adding a new note on a Merge Request' do + let(:noteable) do + create(:merge_request, source_project: project, + target_project: project, author: author) + end before do - merge = create(:merge_request, source_project: project, target_project: project, author: author) - create(:note, note: 'Ultralight Beam', noteable: merge, - project: project, author: participant) - visit_merge_request(project, merge) + visit namespace_project_merge_request_path(project.namespace, project, noteable) end - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end + include_examples "open suggestions when typing @" end - context 'adding a new note on a Commit ', js: true do - let(:commit) { project.commit } + context 'adding a new note on a Commit' do + let(:noteable) { project.commit } + let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) } before do - allow(commit).to receive(:author).and_return(author) - create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA') - visit_commit(project, commit) - end - - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end - end + allow_any_instance_of(Commit).to receive(:author).and_return(author) - def open_member_suggestions - page.within('.new-note') do - find('#note_note').send_keys('@') + visit namespace_project_commit_path(project.namespace, project, noteable) end - wait_for_ajax - end - - def visit_issue(project, issue) - visit namespace_project_issue_path(project.namespace, project, issue) - end - - def visit_merge_request(project, merge) - visit namespace_project_merge_request_path(project.namespace, project, merge) - end - def visit_commit(project, commit) - visit namespace_project_commit_path(project.namespace, project, commit) + include_examples "open suggestions when typing @" end end diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb 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/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index 03d08c12612..5686868a0c4 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' feature 'New blob creation', feature: true, js: true do include WaitForAjax + include TargetBranchHelpers given(:user) { create(:user) } given(:role) { :developer } @@ -20,19 +21,6 @@ feature 'New blob creation', feature: true, js: true do execute_script("ace.edit('editor').setValue('#{content}')") end - def select_branch_index(index) - first('button.js-target-branch').click - wait_for_ajax - all('a[data-group="Branches"]')[index].click - end - - def create_new_branch(name) - first('button.js-target-branch').click - click_link 'Create new branch' - fill_in 'new_branch_name', with: name - click_button 'Create' - end - def commit_file click_button 'Commit Changes' end @@ -53,12 +41,12 @@ feature 'New blob creation', feature: true, js: true do context 'with different target branch' do background do edit_file - select_branch_index(0) + select_branch('feature') commit_file end scenario 'creates the blob in the different branch' do - expect(page).to have_content 'test' + expect(page).to have_content 'feature' expect(page).to have_content 'successfully created' end end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 030043d14aa..b2a3b111c9e 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -53,6 +53,7 @@ describe "Compare", js: true do dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown.find(".compare-dropdown-toggle").click dropdown.fill_in("Filter by Git revision", with: selection) - find_link(selection, visible: true).click + wait_for_ajax + dropdown.find_all("a[data-ref=\"#{selection}\"]", visible: true).last.click end end diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb new file mode 100644 index 00000000000..0b997f130ea --- /dev/null +++ b/spec/features/projects/deploy_keys_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Project deploy keys', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project_empty_repo) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'removing key' do + before do + create(:deploy_keys_project, project: project) + end + + it 'removes association between project and deploy key' do + visit namespace_project_settings_repository_path(project.namespace, project) + + page.within '.deploy-keys' do + expect { click_on 'Remove' } + .to change { project.deploy_keys.count }.by(-1) + end + end + end +end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index e2d16e0830a..acc3efe04e6 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -166,6 +166,25 @@ feature 'Environment', :feature do end end + feature 'environment folders', :js do + context 'when folder name contains special charaters' do + before do + create(:environment, project: project, + name: 'staging-1.0/review', + state: :available) + + visit folder_namespace_project_environments_path(project.namespace, + project, + id: 'staging-1.0') + end + + it 'renders a correct environment folder' do + expect(page).to have_http_status(:ok) + expect(page).to have_content('Environments / staging-1.0') + end + end + end + feature 'auto-close environment when branch is deleted' do given(:project) { create(:project) } diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 8b302a6aa23..c969acc9140 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Project group links', feature: true, js: true do +feature 'Project group links', :feature, :js do include Select2Helper let(:master) { create(:user) } @@ -8,7 +8,7 @@ feature 'Project group links', feature: true, js: true do let!(:group) { create(:group) } background do - project.team << [master, :master] + project.add_master(master) login_as(master) end @@ -29,4 +29,46 @@ feature 'Project group links', feature: true, js: true do end end end + + context 'nested group project' do + let!(:nested_group) { create(:group, parent: group) } + let!(:another_group) { create(:group) } + let!(:project) { create(:project, namespace: nested_group) } + + background do + group.add_master(master) + another_group.add_master(master) + end + + it 'does not show ancestors' do + visit namespace_project_settings_members_path(project.namespace, project) + + click_link 'Search for a group' + + page.within '.select2-drop' do + expect(page).to have_content(another_group.name) + expect(page).not_to have_content(group.name) + end + end + end + + describe 'the groups dropdown' do + before do + group_two = create(:group) + group.add_owner(master) + group_two.add_owner(master) + + visit namespace_project_settings_members_path(project.namespace, project) + execute_script 'GroupsSelect.PER_PAGE = 1;' + open_select2 '#link_group_id' + end + + it 'should infinitely scroll' do + expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1) + + scroll_select2_to_bottom('.select2-drop .select2-results:visible') + + expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2) + end + end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 20cdfbae24f..399c1d478c5 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index b64c15e0adc..de25d45f447 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -61,7 +61,7 @@ feature 'Projects > Members > User requests access', feature: true do click_link('Settings') end - page.within('.page-with-layout-nav .sub-nav') do + page.within('.sub-nav') do click_link('Members') end end diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index df229d0aa78..dab78fd3571 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -23,12 +23,14 @@ feature 'Project milestone', :feature do end it 'shows issues stats' do - expect(page).to have_content 'issues:' + expect(find('.milestone-sidebar')).to have_content 'Issues 0' end - it 'shows Browse Issues button' do - within('#content-body') do - expect(page).to have_link 'Browse Issues' + it 'shows link to browse and add issues' do + within('.milestone-sidebar') do + expect(page).to have_link 'New issue' + expect(page).to have_link 'Open: 0' + expect(page).to have_link 'Closed: 0' end end end @@ -48,12 +50,12 @@ feature 'Project milestone', :feature do end it 'hides issues stats' do - expect(page).to have_no_content 'issues:' + expect(find('.milestone-sidebar')).not_to have_content 'Issues 0' end - it 'hides Browse Issues button' do - within('#content-body') do - expect(page).not_to have_link 'Browse Issues' + it 'hides new issue button' do + within('.milestone-sidebar') do + expect(page).not_to have_link 'New issue' end end diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb new file mode 100644 index 00000000000..da3eaed707a --- /dev/null +++ b/spec/features/projects/milestones/milestones_sorting_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature 'Milestones sorting', :feature, :js do + include SortingHelper + let(:user) { create(:user) } + let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) } + + before do + # Milestones + create(:milestone, + due_date: 10.days.from_now, + created_at: 2.hours.ago, + title: "aaa", project: project) + create(:milestone, + due_date: 11.days.from_now, + created_at: 1.hour.ago, + title: "bbb", project: project) + login_as(user) + end + + scenario 'visit project milestones and sort by due_date_asc' do + visit namespace_project_milestones_path(project.namespace, project) + + expect(page).to have_button('Due soon') + + # assert default sorting + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('aaa') + expect(page.all('ul.content-list > li').last.text).to include('bbb') + end + + click_button 'Due soon' + + sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text) + + expect(sort_options[0]).to eq('Due soon') + expect(sort_options[1]).to eq('Due later') + expect(sort_options[2]).to eq('Start soon') + expect(sort_options[3]).to eq('Start later') + expect(sort_options[4]).to eq('Name, ascending') + expect(sort_options[5]).to eq('Name, descending') + + click_link 'Due later' + + expect(page).to have_button('Due later') + + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('bbb') + expect(page.all('ul.content-list > li').last.text).to include('aaa') + end + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 9f06e52ab55..5a53e48f5f8 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -45,7 +45,7 @@ describe 'Pipeline', :feature, :js do include_context 'pipeline builds' let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) } before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 22bf1bfbdf0..2272b19bc8f 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do end it 'indicates that pipeline can be canceled' do - expect(page).to have_link('Cancel') + expect(page).to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-running') end context 'when canceling' do - before { click_link('Cancel') } + before do + find('.js-pipelines-cancel-button').click + wait_for_vue_resource + end it 'indicated that pipelines was canceled' do - expect(page).not_to have_link('Cancel') + expect(page).not_to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-canceled') end end @@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do end it 'indicates that pipeline can be retried' do - expect(page).to have_link('Retry') + expect(page).to have_selector('.js-pipelines-retry-button') expect(page).to have_selector('.ci-failed') end context 'when retrying' do - before { click_link('Retry') } + before do + find('.js-pipelines-retry-button').click + wait_for_vue_resource + end it 'shows running pipeline that is not retryable' do - expect(page).not_to have_link('Retry') + expect(page).not_to have_selector('.js-pipelines-retry-button') expect(page).to have_selector('.ci-running') end end @@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do it 'has link to the manual action' do find('.js-pipeline-dropdown-manual-actions').click - expect(page).to have_link('manual build') + expect(page).to have_button('manual build') end context 'when manual action was played' do before do find('.js-pipeline-dropdown-manual-actions').click - click_link('manual build') + click_button('manual build') end it 'enqueues manual action job' do - expect(manual.reload).to be_pending + expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled') end end end @@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do before { visit_project_pipelines } it 'is cancelable' do - expect(page).to have_link('Cancel') + expect(page).to have_selector('.js-pipelines-cancel-button') end it 'has pipeline running' do @@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do end context 'when canceling' do - before { click_link('Cancel') } + before { find('.js-pipelines-cancel-button').trigger('click') } it 'indicates that pipeline was canceled' do - expect(page).not_to have_link('Cancel') + expect(page).not_to have_selector('.js-pipelines-cancel-button') expect(page).to have_selector('.ci-canceled') end end @@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do end it 'is not retryable' do - expect(page).not_to have_link('Retry') + expect(page).not_to have_selector('.js-pipelines-retry-button') end it 'has failed pipeline' do @@ -436,7 +442,7 @@ describe 'Pipelines', :feature, :js do context 'when project is public' do let(:project) { create(:project, :public) } - it { expect(page).to have_content 'No pipelines to show' } + it { expect(page).to have_content 'Build with confidence' } it { expect(page).to have_http_status(:success) } end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 24d22a092d4..dc3854262e7 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -7,7 +7,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do let(:mattermost_enabled) { true } before do - Settings.mattermost['enabled'] = mattermost_enabled + stub_mattermost_setting(enabled: mattermost_enabled) project.team << [user, :master] login_as(user) visit edit_namespace_project_service_path(project.namespace, project, service) diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 16541f51d98..c0a4a1e4bf5 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do background do service.fields - service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7) + service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7) project.team << [user, :master] login_as(user) end @@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do expect(page.find_field("service_merge_request_channel").value).to have_content '3' expect(page.find_field("service_note_channel").value).to have_content '4' expect(page.find_field("service_tag_push_channel").value).to have_content '5' - expect(page.find_field("service_build_channel").value).to have_content '6' + expect(page.find_field("service_pipeline_channel").value).to have_content '6' expect(page.find_field("service_wiki_page_channel").value).to have_content '7' end end diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb new file mode 100644 index 00000000000..2065abfb248 --- /dev/null +++ b/spec/features/projects/user_create_dir_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +feature 'New directory creation', feature: true, js: true do + include WaitForAjax + include TargetBranchHelpers + + given(:user) { create(:user) } + given(:role) { :developer } + given(:project) { create(:project) } + + background do + login_as(user) + project.team << [user, role] + visit namespace_project_tree_path(project.namespace, project, 'master') + open_new_directory_modal + fill_in 'dir_name', with: 'new_directory' + end + + def open_new_directory_modal + first('.add-to-tree').click + click_link 'New directory' + end + + def create_directory + click_button 'Create directory' + end + + context 'with default target branch' do + background do + create_directory + end + + scenario 'creates the directory in the default branch' do + expect(page).to have_content 'master' + expect(page).to have_content 'The directory has been successfully created' + expect(page).to have_content 'new_directory' + end + end + + context 'with different target branch' do + background do + select_branch('feature') + create_directory + end + + scenario 'creates the directory in the different branch' do + expect(page).to have_content 'feature' + expect(page).to have_content 'The directory has been successfully created' + end + end + + context 'with a new target branch' do + given(:new_branch_name) { 'new-feature' } + + background do + create_new_branch(new_branch_name) + create_directory + end + + scenario 'creates the directory in the new branch' do + expect(page).to have_content new_branch_name + expect(page).to have_content 'The directory has been successfully created' + end + + scenario 'redirects to the merge request' do + expect(page).to have_content 'New Merge Request' + expect(page).to have_content "From #{new_branch_name} into master" + expect(page).to have_content 'Add new directory' + expect(current_path).to eq(new_namespace_project_merge_request_path(project.namespace, project)) + end + end +end diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb new file mode 100644 index 00000000000..6825b95c8aa --- /dev/null +++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Projects > Wiki > User views Git access wiki page', :feature do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:wiki_page) do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + before do + login_as(user) + end + + scenario 'Visit Wiki Page Current Commit' do + visit namespace_project_wiki_path(project.namespace, project, wiki_page) + + click_link 'Clone repository' + expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}") + expect(page).to have_text(project.wiki.http_url_to_repo(user)) + end +end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index f842d14fa96..aedc0333cb9 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do context 'in the user namespace' do let(:project) { create(:project, namespace: user.namespace) } - scenario 'the home page' do - click_link 'Edit' - - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Save changes' - - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + context 'the home page' do + scenario 'success when the wiki content is not empty' do + click_link 'Edit' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Save changes' + + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'failure when the wiki content is empty' do + click_link 'Edit' + + fill_in :wiki_content, with: '' + click_button 'Save changes' + + expect(page).to have_selector('.wiki-form') + expect(page).to have_content('Edit Page') + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content('Content can\'t be blank') + expect(find('textarea#wiki_content').value).to eq '' + end end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 850020109d4..c270511c903 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -251,7 +251,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message' do - within('.todos-pending-count') { expect(page).to have_content '0' } + within('.todos-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content 'Done 0' expect(page).to have_selector('.todos-all-done', count: 1) @@ -267,7 +267,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows 99+ for count >= 100 in notification' do - expect(page).to have_selector('.todos-pending-count', text: '99+') + expect(page).to have_selector('.todos-count', text: '99+') end it 'shows exact number in To do tab' do @@ -277,7 +277,7 @@ describe 'Dashboard Todos', feature: true do it 'shows exact number for count < 100' do 3.times { first('.js-done-todo').click } - expect(page).to have_selector('.todos-pending-count', text: '98') + expect(page).to have_selector('.todos-count', text: '98') end end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb index 336c4092c98..848af5e3a4d 100644 --- a/spec/features/user_callout_spec.rb +++ b/spec/features/user_callout_spec.rb @@ -2,19 +2,32 @@ require 'spec_helper' describe 'User Callouts', js: true do let(:user) { create(:user) } + let(:another_user) { create(:user) } let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } before do login_as(user) - project.team << [user, :master] + project.team << [user, :master] end - it 'takes you to the profile preferences when the link is clicked' do + it 'takes you to the profile preferences when the link is clicked' do visit dashboard_projects_path click_link 'Check it out' expect(current_path).to eq profile_preferences_path end + it 'does not show when cookie is set' do + visit dashboard_projects_path + + within('.user-callout') do + find('.close').click + end + + visit dashboard_projects_path + + expect(page).not_to have_selector('.user-callout') + end + describe 'user callout should appear in two routes' do it 'shows up on the user profile' do visit user_path(user) @@ -30,8 +43,13 @@ describe 'User Callouts', js: true do it 'hides the user callout when click on the dismiss icon' do visit user_path(user) within('.user-callout') do - find('.close-user-callout').click + find('.close').click end - expect(page).not_to have_selector('#user-callout') + expect(page).not_to have_selector('.user-callout') + end + + it 'does not show callout on another users profile' do + visit user_path(another_user) + expect(page).not_to have_selector('.user-callout') end end diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb new file mode 100644 index 00000000000..1d75fe434b0 --- /dev/null +++ b/spec/features/users/projects_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'Projects tab on a user profile', :feature, :js do + include WaitForAjax + + let(:user) { create(:user) } + let!(:project) { create(:empty_project, namespace: user.namespace) } + let!(:project2) { create(:empty_project, namespace: user.namespace) } + + before do + allow(Project).to receive(:default_per_page).and_return(1) + + login_as(user) + + visit user_path(user) + + page.within('.user-profile-nav') do + click_link('Personal projects') + end + + wait_for_ajax + end + + it 'paginates results' do + expect(page).to have_content(project2.name) + + click_link('Next') + + expect(page).to have_content(project.name) + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index ee52dc65175..231fd85c464 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -9,7 +9,7 @@ describe IssuesFinder do let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } - let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } + let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') } describe '#execute' do let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 819287bf919..11a4caf6628 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["label", "done"] + "enum": ["label", "closed"] }, "label": { "type": ["object", "null"], diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index cd3281d6f51..a0e1265efff 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -62,4 +62,18 @@ describe AuthHelper do end end end + + describe 'unlink_allowed?' do + [:saml, :cas3].each do |provider| + it "returns true if the provider is #{provider}" do + expect(helper.unlink_allowed?(provider)).to be false + end + end + + [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0].each do |provider| + it "returns false if the provider is #{provider}" do + expect(helper.unlink_allowed?(provider)).to be true + end + end + end end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb new file mode 100644 index 00000000000..581726c1d0e --- /dev/null +++ b/spec/helpers/avatars_helper_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe AvatarsHelper do + let(:user) { create(:user) } + + describe '#user_avatar' do + subject { helper.user_avatar(user: user) } + + it "links to the user's profile" do + is_expected.to include("href=\"#{user_path(user)}\"") + end + + it "has the user's name as title" do + is_expected.to include("title=\"#{user.name}\"") + end + + it "contains the user's avatar image" do + is_expected.to include(CGI.escapeHTML(user.avatar_url(16))) + end + end +end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 87653631c28..3cb809d42b5 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -49,8 +49,12 @@ describe MilestonesHelper do end describe '#milestone_remaining_days' do + around do |example| + Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } + end + context 'when less than 31 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.2.days.from_now)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } it 'returns days remaining' do expect(milestone_remaining).to eq("<strong>12</strong> days remaining") @@ -58,7 +62,7 @@ describe MilestonesHelper do end context 'when less than 1 year and more than 30 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } it 'returns months remaining' do expect(milestone_remaining).to eq("<strong>2</strong> months remaining") @@ -66,7 +70,7 @@ describe MilestonesHelper do end context 'when more than 1 year remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 1.year.from_now + 2.days)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } it 'returns years remaining' do expect(milestone_remaining).to eq("<strong>1</strong> year remaining") @@ -74,7 +78,7 @@ describe MilestonesHelper do end context 'when milestone is expired' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } it 'returns "Past due"' do expect(milestone_remaining).to eq("<strong>Past due</strong>") @@ -82,7 +86,7 @@ describe MilestonesHelper do end context 'when milestone has start_date in the future' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } it 'returns "Upcoming"' do expect(milestone_remaining).to eq("<strong>Upcoming</strong>") @@ -90,7 +94,7 @@ describe MilestonesHelper do end context 'when milestone has start_date in the past' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago)) } + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } it 'returns days elapsed' do expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb new file mode 100644 index 00000000000..e5143a0263d --- /dev/null +++ b/spec/helpers/namespaces_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe NamespacesHelper, type: :helper do + let!(:admin) { create(:admin) } + let!(:admin_group) { create(:group, :private) } + let!(:user) { create(:user) } + let!(:user_group) { create(:group, :private) } + + before do + admin_group.add_owner(admin) + user_group.add_owner(user) + end + + describe '#namespaces_options' do + it 'returns groups without being a member for admin' do + allow(helper).to receive(:current_user).and_return(admin) + + options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id) + + expect(options).to include(admin_group.name) + expect(options).to include(user_group.name) + end + + it 'returns only allowed namespaces for user' do + allow(helper).to receive(:current_user).and_return(user) + + options = helper.namespaces_options + + expect(options).not_to include(admin_group.name) + expect(options).to include(user_group.name) + end + end +end diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb index f86e496740a..117abc9c556 100644 --- a/spec/helpers/sidekiq_helper_spec.rb +++ b/spec/helpers/sidekiq_helper_spec.rb @@ -53,6 +53,14 @@ describe SidekiqHelper do expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]']) end + it 'parses OpenBSD output' do + # OpenBSD 6.1 + line = '49258 0.5 2.3 R/0 Fri10PM ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['49258', '0.5', '2.3', 'R/0', 'Fri10PM', 'ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)']) + end + it 'does fail gracefully on line not matching the format' do line = '55137 10.0 2.1 S+ 2:30pm something' parts = helper.parse_sidekiq_ps(line) diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 00000000000..03f78de8e91 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe UsersHelper do + let(:user) { create(:user) } + + describe '#user_link' do + subject { helper.user_link(user) } + + it "links to the user's profile" do + is_expected.to include("href=\"#{user_path(user)}\"") + end + + it "has the user's email as title" do + is_expected.to include("title=\"#{user.email}\"") + end + end +end diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js new file mode 100644 index 00000000000..11f2a950678 --- /dev/null +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -0,0 +1,159 @@ +import Vue from 'vue'; +import renderNotebook from '~/blob/notebook'; + +describe('iPython notebook renderer', () => { + preloadFixtures('static/notebook_viewer.html.raw'); + + beforeEach(() => { + loadFixtures('static/notebook_viewer.html.raw'); + }); + + it('shows loading icon', () => { + renderNotebook(); + + expect( + document.querySelector('.loading'), + ).not.toBeNull(); + }); + + describe('successful response', () => { + const response = (request, next) => { + next(request.respondWith(JSON.stringify({ + cells: [{ + cell_type: 'markdown', + source: ['# test'], + }, { + cell_type: 'code', + execution_count: 1, + source: [ + 'def test(str)', + ' return str', + ], + outputs: [], + }], + }), { + status: 200, + })); + }; + + beforeEach((done) => { + Vue.http.interceptors.push(response); + + renderNotebook(); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, response, + ); + }); + + it('does not show loading icon', () => { + expect( + document.querySelector('.loading'), + ).toBeNull(); + }); + + it('renders the notebook', () => { + expect( + document.querySelector('.md'), + ).not.toBeNull(); + }); + + it('renders the markdown cell', () => { + expect( + document.querySelector('h1'), + ).not.toBeNull(); + + expect( + document.querySelector('h1').textContent.trim(), + ).toBe('test'); + }); + + it('highlights code', () => { + expect( + document.querySelector('.token'), + ).not.toBeNull(); + + expect( + document.querySelector('.language-python'), + ).not.toBeNull(); + }); + }); + + describe('error in JSON response', () => { + const response = (request, next) => { + next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', { + status: 200, + })); + }; + + beforeEach((done) => { + Vue.http.interceptors.push(response); + + renderNotebook(); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, response, + ); + }); + + it('does not show loading icon', () => { + expect( + document.querySelector('.loading'), + ).toBeNull(); + }); + + it('shows error message', () => { + expect( + document.querySelector('.md').textContent.trim(), + ).toBe('An error occured whilst parsing the file.'); + }); + }); + + describe('error getting file', () => { + const response = (request, next) => { + next(request.respondWith('', { + status: 500, + })); + }; + + beforeEach((done) => { + Vue.http.interceptors.push(response); + + renderNotebook(); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, response, + ); + }); + + it('does not show loading icon', () => { + expect( + document.querySelector('.loading'), + ).toBeNull(); + }); + + it('shows error message', () => { + expect( + document.querySelector('.md').textContent.trim(), + ).toBe('An error occured whilst loading the file. Please try again later.'); + }); + }); +}); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index be31f644e20..de072e7e470 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -1,10 +1,13 @@ -/* global Vue */ /* global List */ +/* global ListUser */ /* global ListLabel */ /* global listObj */ /* global boardsMockInterceptor */ /* global BoardService */ +import Vue from 'vue'; +import '~/boards/models/user'; + require('~/boards/models/list'); require('~/boards/models/label'); require('~/boards/stores/boards_store'); @@ -129,6 +132,23 @@ describe('Issue card', () => { expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); }); + it('does not set detail issue if img is clicked', (done) => { + vm.issue.assignee = new ListUser({ + id: 1, + name: 'testing 123', + username: 'test', + avatar: 'test_image', + }); + + Vue.nextTick(() => { + triggerEvent('mouseup', vm.$el.querySelector('img')); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + + done(); + }); + }); + it('does not set detail issue if showDetail is false after mouseup', () => { triggerEvent('mouseup'); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 1d1069600fc..b55ff2f473a 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,12 +1,13 @@ /* eslint-disable comma-dangle, one-var, no-unused-vars */ -/* global Vue */ /* global BoardService */ /* global boardsMockInterceptor */ -/* global Cookies */ /* global listObj */ /* global listObjDuplicate */ /* global ListIssue */ +import Vue from 'vue'; +import Cookies from 'js-cookie'; + require('~/lib/utils/url_utility'); require('~/boards/models/issue'); require('~/boards/models/label'); @@ -49,9 +50,9 @@ describe('Store', () => { it('finds list by ID', () => { gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); }); it('finds list by type', () => { @@ -63,7 +64,7 @@ describe('Store', () => { it('gets issue when new list added', (done) => { gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); @@ -88,9 +89,9 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); setTimeout(() => { - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(list).toBeDefined(); - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); expect(list.position).toBe(0); done(); }, 0); @@ -105,9 +106,9 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); }); - it('check for blank state adding when done list exist', () => { + it('check for blank state adding when closed list exist', () => { gl.issueBoards.BoardsStore.addList({ - list_type: 'done' + list_type: 'closed' }); expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); @@ -125,7 +126,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - gl.issueBoards.BoardsStore.removeList(1, 'label'); + gl.issueBoards.BoardsStore.removeList(listObj.id, 'label'); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); }); @@ -136,7 +137,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']); + gl.issueBoards.BoardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]); expect(listOne.position).toBe(1); }); diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 4340a571017..1a5e9e9fd07 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -1,9 +1,10 @@ -/* global Vue */ /* global ListUser */ /* global ListLabel */ /* global listObj */ /* global ListIssue */ +import Vue from 'vue'; + require('~/boards/models/issue'); require('~/boards/models/label'); require('~/boards/models/list'); diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d49d3af33d9..a9d4c6ef76f 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle */ -/* global Vue */ /* global boardsMockInterceptor */ /* global BoardService */ /* global List */ @@ -7,6 +6,8 @@ /* global listObj */ /* global listObjDuplicate */ +import Vue from 'vue'; + require('~/lib/utils/url_utility'); require('~/boards/models/issue'); require('~/boards/models/label'); @@ -42,7 +43,7 @@ describe('List model', () => { list = new List({ title: 'test', label: { - id: 1, + id: _.random(10000), title: 'test', color: 'red' } @@ -50,7 +51,7 @@ describe('List model', () => { list.save(); setTimeout(() => { - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); expect(list.type).toBe('label'); expect(list.position).toBe(0); done(); @@ -59,7 +60,7 @@ describe('List model', () => { it('destroys the list', (done) => { gl.issueBoards.BoardsStore.addList(listObj); - list = gl.issueBoards.BoardsStore.findList('id', 1); + list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); list.destroy(); @@ -91,7 +92,7 @@ describe('List model', () => { const listDup = new List(listObjDuplicate); const issue = new ListIssue({ title: 'Testing', - iid: 1, + iid: _.random(10000), confidential: false, labels: [list.label, listDup.label] }); @@ -101,7 +102,7 @@ describe('List model', () => { spyOn(gl.boardService, 'moveIssue').and.callThrough(); - listDup.updateIssueLabel(list, issue); + listDup.updateIssueLabel(issue, list); expect(gl.boardService.moveIssue) .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined); diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 7a399b307ad..a4fa694eebe 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,12 +1,12 @@ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ const listObj = { - id: 1, + id: _.random(10000), position: 0, title: 'Test', list_type: 'label', label: { - id: 1, + id: _.random(10000), title: 'Testing', color: 'red', description: 'testing;' @@ -14,12 +14,12 @@ const listObj = { }; const listObjDuplicate = { - id: 2, + id: listObj.id, position: 1, title: 'Test', list_type: 'label', label: { - id: 2, + id: listObj.label.id, title: 'Testing', color: 'red', description: 'testing;' diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 1815847f3fa..80db816aff8 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -1,4 +1,3 @@ -/* global Vue */ /* global ListIssue */ require('~/boards/models/issue'); diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js new file mode 100644 index 00000000000..974815fe939 --- /dev/null +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -0,0 +1,123 @@ +/* global Sidebar */ +/* eslint-disable no-new */ +import _ from 'underscore'; +import '~/right_sidebar'; + +describe('Issuable right sidebar collapsed todo toggle', () => { + const fixtureName = 'issues/open-issue.html.raw'; + const jsonFixtureName = 'todos/todos.json'; + + preloadFixtures(fixtureName); + preloadFixtures(jsonFixtureName); + + beforeEach(() => { + const todoData = getJSONFixture(jsonFixtureName); + new Sidebar(); + loadFixtures(fixtureName); + + document.querySelector('.js-right-sidebar') + .classList.toggle('right-sidebar-expanded'); + document.querySelector('.js-right-sidebar') + .classList.toggle('right-sidebar-collapsed'); + + spyOn(jQuery, 'ajax').and.callFake((res) => { + const d = $.Deferred(); + const response = _.clone(todoData); + + if (res.type === 'DELETE') { + delete response.delete_path; + } + + d.resolve(response); + return d.promise(); + }); + }); + + it('shows add todo button', () => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'), + ).not.toBeNull(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-plus-square'), + ).not.toBeNull(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).toBeNull(); + }); + + it('sets default tooltip title', () => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'), + ).toBe('Add todo'); + }); + + it('toggle todo state', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'), + ).not.toBeNull(); + }); + + it('toggle todo state of expanded todo toggle', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Mark done'); + }); + + it('toggles todo button tooltip', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), + ).toBe('Mark done'); + }); + + it('marks todo as done', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).toBeNull(); + + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Add todo'); + }); + + it('updates aria-label to mark done', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + }); + + it('updates aria-label to add todo', () => { + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Add todo'); + }); +}); diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js index 188908d66bd..82b00b4c1ec 100644 --- a/spec/javascripts/commit/pipelines/mock_data.js +++ b/spec/javascripts/commit/pipelines/mock_data.js @@ -1,5 +1,4 @@ -/* eslint-disable no-unused-vars */ -const pipeline = { +export default { id: 73, user: { name: 'Administrator', @@ -88,5 +87,3 @@ const pipeline = { created_at: '2017-01-16T17:13:59.800Z', updated_at: '2017-01-25T00:00:17.132Z', }; - -module.exports = pipeline; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index f09c57978a1..8cac3cad232 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,11 +1,6 @@ -/* global pipeline, Vue */ - -require('~/flash'); -require('~/commit/pipelines/pipelines_store'); -require('~/commit/pipelines/pipelines_service'); -require('~/commit/pipelines/pipelines_table'); -require('~/vue_shared/vue_resource_interceptor'); -const pipeline = require('./mock_data'); +import Vue from 'vue'; +import PipelinesTable from '~/commit/pipelines/pipelines_table'; +import pipeline from './mock_data'; describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures('static/pipelines_table.html.raw'); @@ -14,7 +9,7 @@ describe('Pipelines table in Commits and Merge requests', () => { loadFixtures('static/pipelines_table.html.raw'); }); - describe('successfull request', () => { + describe('successful request', () => { describe('without pipelines', () => { const pipelinesEmptyResponse = (request, next) => { next(request.respondWith(JSON.stringify([]), { @@ -22,23 +17,25 @@ describe('Pipelines table in Commits and Merge requests', () => { })); }; - beforeEach(() => { + beforeEach(function () { Vue.http.interceptors.push(pipelinesEmptyResponse); + + this.component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); - afterEach(() => { + afterEach(function () { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesEmptyResponse, ); + this.component.$destroy(); }); - it('should render the empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - + it('should render the empty state', function (done) { setTimeout(() => { - expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + expect(this.component.$el.querySelector('.empty-state')).toBeDefined(); + expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 1); }); @@ -53,21 +50,23 @@ describe('Pipelines table in Commits and Merge requests', () => { beforeEach(() => { Vue.http.interceptors.push(pipelinesResponse); + + this.component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); afterEach(() => { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesResponse, ); + this.component.$destroy(); }); it('should render a table with the received pipelines', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - setTimeout(() => { - expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 0); }); @@ -81,23 +80,25 @@ describe('Pipelines table in Commits and Merge requests', () => { })); }; - beforeEach(() => { + beforeEach(function () { Vue.http.interceptors.push(pipelinesErrorResponse); + + this.component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); - afterEach(() => { + afterEach(function () { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesErrorResponse, ); + this.component.$destroy(); }); - it('should render empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - + it('should render empty state', function (done) { setTimeout(() => { - expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); + expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 0); }); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js b/spec/javascripts/commit/pipelines/pipelines_store_spec.js deleted file mode 100644 index 94973419979..00000000000 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -const PipelinesStore = require('~/commit/pipelines/pipelines_store'); - -describe('Store', () => { - let store; - - beforeEach(() => { - store = new PipelinesStore(); - }); - - // unregister intervals and event handlers - afterEach(() => gl.VueRealtimeListener.reset()); - - it('should start with a blank state', () => { - expect(store.state.pipelines.length).toBe(0); - }); - - it('should store an array of pipelines', () => { - const pipelines = [ - { - id: '1', - name: 'pipeline', - }, - { - id: '2', - name: 'pipeline_2', - }, - ]; - - store.storePipelines(pipelines); - - expect(store.state.pipelines.length).toBe(pipelines.length); - }); -}); diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js new file mode 100644 index 00000000000..50000c5a5f5 --- /dev/null +++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; + +describe('Limit warning component', () => { + let component; + let LimitWarningComponent; + + beforeEach(() => { + LimitWarningComponent = Vue.extend(limitWarningComp); + }); + + it('should not render if count is not exactly than 50', () => { + component = new LimitWarningComponent({ + propsData: { + count: 5, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe(''); + + component = new LimitWarningComponent({ + propsData: { + count: 55, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe(''); + }); + + it('should render if count is exactly 50', () => { + component = new LimitWarningComponent({ + propsData: { + count: 50, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe('Showing 50 events'); + }); +}); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 85b73f1d4e2..13840b42bd6 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -32,7 +32,12 @@ describe('Actions Component', () => { }).$mount(); }); - it('should render a dropdown with the provided actions', () => { + it('should render a dropdown button with icon and title attribute', () => { + expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); + expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...'); + }); + + it('should render a dropdown with the provided list of actions', () => { expect( component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js new file mode 100644 index 00000000000..fc451cce641 --- /dev/null +++ b/spec/javascripts/environments/environment_monitoring_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import monitoringComp from '~/environments/components/environment_monitoring'; + +describe('Monitoring Component', () => { + let MonitoringComponent; + + beforeEach(() => { + MonitoringComponent = Vue.extend(monitoringComp); + }); + + it('should render a link to environment monitoring page', () => { + const monitoringUrl = 'https://gitlab.com'; + const component = new MonitoringComponent({ + propsData: { + monitoringUrl, + }, + }).$mount(); + + expect(component.$el.getAttribute('href')).toEqual(monitoringUrl); + expect(component.$el.querySelector('.fa-area-chart')).toBeDefined(); + expect(component.$el.getAttribute('title')).toEqual('Monitoring'); + }); +}); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index 9601575577e..9bcf617fcd8 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -91,6 +91,10 @@ describe('Environment', () => { }); describe('pagination', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + it('should render pagination', (done) => { setTimeout(() => { expect( diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 8f79b88f3df..01055e3f255 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -24,7 +24,7 @@ describe('Stop Component', () => { it('should render a button to stop the environment', () => { expect(component.$el.tagName).toEqual('BUTTON'); - expect(component.$el.getAttribute('title')).toEqual('Stop Environment'); + expect(component.$el.getAttribute('title')).toEqual('Stop'); }); it('should call the service when an action is clicked', () => { diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index b07aa4e1745..be2289edc2b 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -18,7 +18,7 @@ describe('Stop Component', () => { it('should render a link to open a web terminal with the provided path', () => { expect(component.$el.tagName).toEqual('A'); - expect(component.$el.getAttribute('title')).toEqual('Open web terminal'); + expect(component.$el.getAttribute('title')).toEqual('Terminal'); expect(component.$el.getAttribute('href')).toEqual(terminalPath); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index ae9c263d1d7..5f7c05e9014 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -58,7 +58,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper }); describe('search', () => { - const defaultParams = '?scope=all&utf8=✓&state=opened'; + const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; it('should search with a single word', (done) => { input.value = 'searchTerm'; @@ -92,6 +92,20 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper manager.search(); }); + + it('removes duplicated tokens', (done) => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `); + + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&label_name[]=bug`); + done(); + }); + + manager.search(); + }); }); describe('handleInputPlaceholder', () => { @@ -246,5 +260,17 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); }); }); + + describe('toggleInputContainerFocus', () => { + it('toggles on focus', () => { + input.focus(); + expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true); + }); + + it('toggles on blur', () => { + input.blur(); + expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false); + }); + }); }); })(); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index a91801cfc89..cabbc694ec4 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -122,6 +122,14 @@ require('~/filtered_search/filtered_search_tokenizer'); expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes'); }); + + it('removes duplicated values', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('foo'); + expect(results.tokens[0].symbol).toBe('~'); + }); }); }); })(); diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb new file mode 100644 index 00000000000..e83db8daaf2 --- /dev/null +++ b/spec/javascripts/fixtures/dashboard.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Dashboard::ProjectsController, '(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: 'builds-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('dashboard/') + end + + before(:each) do + sign_in(admin) + end + + it 'dashboard/user-callout.html.raw' do |example| + rendered = render_template('shared/_user_callout') + store_frontend_fixture(rendered, example.description) + end + + private + + def render_template(template_file_name) + controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH) + controller.render_to_string(template_file_name, layout: false) + end +end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index ee893b76c84..fddeaaf504d 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -6,6 +6,15 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:pipeline) do + create( + :ci_pipeline, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha + ) + end render_views @@ -18,7 +27,8 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont 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') + create(:ci_build, :pending, pipeline: pipeline) + render_merge_request(example.description, merge_request) end diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml new file mode 100644 index 00000000000..17a7a9d8f31 --- /dev/null +++ b/spec/javascripts/fixtures/notebook_viewer.html.haml @@ -0,0 +1 @@ +.file-content#js-notebook-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml new file mode 100644 index 00000000000..418a38a0e2e --- /dev/null +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -0,0 +1,14 @@ +%div + #pipelines-list-vue{ data: { endpoint: 'foo', + "css-class" => 'foo', + "help-page-path" => 'foo', + "new-pipeline-path" => 'foo', + "can-create-pipeline" => 'true', + "all-path" => 'foo', + "pending-path" => 'foo', + "running-path" => 'foo', + "finished-path" => 'foo', + "branches-path" => 'foo', + "tags-path" => 'foo', + "has-ci" => 'foo', + "ci-lint-path" => 'foo' } } diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml index fbe4a434f76..ad1682704bb 100644 --- a/spec/javascripts/fixtures/pipelines_table.html.haml +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -1,2 +1 @@ -#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } -.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } +#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } } diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml deleted file mode 100644 index 275359bde0a..00000000000 --- a/spec/javascripts/fixtures/user_callout.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.user-callout{ 'callout-svg' => custom_icon('icon_customization') } - diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 46a27b8c98f..b5dde5525e5 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -5,7 +5,7 @@ require('~/lib/utils/text_utility'); (function() { describe('Header', function() { - var todosPendingCount = '.todos-pending-count'; + var todosPendingCount = '.todos-count'; var fixtureTemplate = 'issues/open-issue.html.raw'; function isTodosCountHidden() { @@ -21,31 +21,31 @@ require('~/lib/utils/text_utility'); loadFixtures(fixtureTemplate); }); - it('should update todos-pending-count after receiving the todo:toggle event', function() { + it('should update todos-count after receiving the todo:toggle event', function() { triggerToggle(5); expect($(todosPendingCount).text()).toEqual('5'); }); - it('should hide todos-pending-count when it is 0', function() { + it('should hide todos-count when it is 0', function() { triggerToggle(0); expect(isTodosCountHidden()).toEqual(true); }); - it('should show todos-pending-count when it is more than 0', function() { + it('should show todos-count when it is more than 0', function() { triggerToggle(10); expect(isTodosCountHidden()).toEqual(false); }); - describe('when todos-pending-count is 1000', function() { + describe('when todos-count is 1000', function() { beforeEach(function() { triggerToggle(1000); }); - it('should show todos-pending-count', function() { + it('should show todos-count', function() { expect(isTodosCountHidden()).toEqual(false); }); - it('should show 99+ for todos-pending-count', function() { + it('should show 99+ for todos-count', function() { expect($(todosPendingCount).text()).toEqual('99+'); }); }); diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js index cb068a4f879..0a830f25e29 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js +++ b/spec/javascripts/issuable_time_tracker_spec.js @@ -1,7 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren, func-call-spacing, no-spaced-func, semi, max-len, quotes, space-infix-ops, padded-blocks */ + +import Vue from 'vue'; -require('jquery'); -require('vue'); require('~/issuable/time_tracking/components/time_tracker'); function initTimeTrackingComponent(opts) { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 8d25500b9fd..aabc8bea12f 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -136,6 +136,21 @@ describe('Issue', function() { expectErrorMessage(); expect($('.issue_counter')).toHaveText(1); }); + + it('updates counter', () => { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expectPendingRequest(req, $btnClose); + req.success({ + id: 34 + }); + }); + + expect($('.issue_counter')).toHaveText(1); + $('.issue_counter').text('1,001'); + expect($('.issue_counter').text()).toEqual('1,001'); + $btnClose.trigger('click'); + expect($('.issue_counter').text()).toEqual('1,000'); + }); }); describe('reopen issue', function() { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f4d3e77e515..5a93d479c1f 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -46,6 +46,10 @@ require('~/lib/utils/common_utils'); spyOn(window.document, 'getElementById').and.callThrough(); }); + afterEach(() => { + window.history.pushState({}, null, ''); + }); + function expectGetElementIdToHaveBeenCalledWith(elementId) { expect(window.document.getElementById).toHaveBeenCalledWith(elementId); } @@ -75,11 +79,56 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.setParamInURL', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + + it('should return the parameter', () => { + window.history.replaceState({}, null, ''); + + expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156'); + expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156'); + }); + + it('should update the existing parameter when its a number', () => { + window.history.pushState({}, null, '?page=15'); + + expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16'); + expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16'); + expect(gl.utils.setParamInURL('page', true)).toBe('?page=true'); + }); + + it('should update the existing parameter when its a string', () => { + window.history.pushState({}, null, '?scope=all'); + + expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); + }); + + it('should update the existing parameter when more than one parameter exists', () => { + window.history.pushState({}, null, '?scope=all&page=15'); + + expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); + }); + + it('should add a new parameter to the end of the existing ones', () => { + window.history.pushState({}, null, '?scope=all'); + + expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); + expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); + expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true'); + }); + }); + describe('gl.utils.getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); }); + afterEach(() => { + window.history.replaceState({}, null, null); + }); + it('should return valid parameter', () => { const value = gl.utils.getParameterByName('scope'); expect(value).toBe('all'); @@ -108,6 +157,37 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.normalizeCRLFHeaders', () => { + beforeEach(function () { + this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + + spyOn(String.prototype, 'split').and.callThrough(); + spyOn(gl.utils, 'normalizeHeaders').and.callThrough(); + + this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders); + }); + + it('should split by newline', function () { + expect(String.prototype.split).toHaveBeenCalledWith('\n'); + }); + + it('should split by colon+space for each header', function () { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + }); + + it('should call gl.utils.normalizeHeaders with a parsed headers object', function () { + expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object)); + }); + + it('should return a normalized headers object', function () { + expect(this.normalizeCRLFHeaders).toEqual({ + 'A-HEADER': 'a-value', + 'ANOTHER-HEADER': 'ANOTHER-VALUE', + 'LAST-HEADER': 'last-VALUE', + }); + }); + }); + describe('gl.utils.parseIntPagination', () => { it('should parse to integers all string values and return pagination object', () => { const pagination = { @@ -163,5 +243,72 @@ require('~/lib/utils/common_utils'); expect(gl.utils.isMetaClick(e)).toBe(true); }); }); + + describe('gl.utils.backOff', () => { + it('solves the promise from the callback', (done) => { + const expectedResponseValue = 'Success!'; + gl.utils.backOff((next, stop) => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then((resp) => { + stop(resp); + }) + )).then((respBackoff) => { + expect(respBackoff).toBe(expectedResponseValue); + done(); + }); + }); + + it('catches the rejected promise from the callback ', (done) => { + const errorMessage = 'Mistakes were made!'; + gl.utils.backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }).then((resp) => { + stop(resp); + }).catch(err => stop(err)); + }).catch((errBackoffResp) => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); + }); + }); + + it('solves the promise correctly after retrying a third time', (done) => { + let numberOfCalls = 1; + const expectedResponseValue = 'Success!'; + gl.utils.backOff((next, stop) => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then((resp) => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } + }) + )).then((respBackoff) => { + expect(respBackoff).toBe(expectedResponseValue); + expect(numberOfCalls).toBe(3); + done(); + }); + }, 10000); + + it('rejects the backOff promise after timing out', (done) => { + const expectedResponseValue = 'Success!'; + gl.utils.backOff(next => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then(() => { + setTimeout(next(), 5000); // it will time out + }) + ), 3000).catch((errBackoffResp) => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); + done(); + }); + }, 10000); + }); }); })(); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js new file mode 100644 index 00000000000..e3429c2a1cb --- /dev/null +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -0,0 +1,203 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import Poll from '~/lib/utils/poll'; + +Vue.use(VueResource); + +class ServiceMock { + constructor(endpoint) { + this.service = Vue.resource(endpoint); + } + + fetch() { + return this.service.get(); + } +} + +describe('Poll', () => { + let callbacks; + + beforeEach(() => { + callbacks = { + success: () => {}, + error: () => {}, + }; + + spyOn(callbacks, 'success'); + spyOn(callbacks, 'error'); + }); + + it('calls the success callback when no header for interval is provided', (done) => { + const successInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200 })); + }; + + Vue.http.interceptors.push(successInterceptor); + + new Poll({ + resource: new ServiceMock('endpoint'), + method: 'fetch', + successCallback: callbacks.success, + errorCallback: callbacks.error, + }).makeRequest(); + + setTimeout(() => { + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + done(); + }, 0); + + Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor); + }); + + it('calls the error callback whe the http request returns an error', (done) => { + const errorInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 500 })); + }; + + Vue.http.interceptors.push(errorInterceptor); + + new Poll({ + resource: new ServiceMock('endpoint'), + method: 'fetch', + successCallback: callbacks.success, + errorCallback: callbacks.error, + }).makeRequest(); + + setTimeout(() => { + expect(callbacks.success).not.toHaveBeenCalled(); + expect(callbacks.error).toHaveBeenCalled(); + done(); + }, 0); + + Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor); + }); + + it('should call the success callback when the interval header is -1', (done) => { + const intervalInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } })); + }; + + Vue.http.interceptors.push(intervalInterceptor); + + new Poll({ + resource: new ServiceMock('endpoint'), + method: 'fetch', + successCallback: callbacks.success, + errorCallback: callbacks.error, + }).makeRequest(); + + setTimeout(() => { + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + done(); + }, 0); + + Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor); + }); + + it('starts polling when http status is 200 and interval header is provided', (done) => { + const pollInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); + }; + + Vue.http.interceptors.push(pollInterceptor); + + const service = new ServiceMock('endpoint'); + spyOn(service, 'fetch').and.callThrough(); + + new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: callbacks.success, + errorCallback: callbacks.error, + }).makeRequest(); + + setTimeout(() => { + expect(service.fetch.calls.count()).toEqual(2); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + done(); + }, 5); + + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + }); + + describe('stop', () => { + it('stops polling when method is called', (done) => { + const pollInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); + }; + + Vue.http.interceptors.push(pollInterceptor); + + const service = new ServiceMock('endpoint'); + spyOn(service, 'fetch').and.callThrough(); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => { + Polling.stop(); + }, + errorCallback: callbacks.error, + }); + + spyOn(Polling, 'stop').and.callThrough(); + + Polling.makeRequest(); + + setTimeout(() => { + expect(service.fetch.calls.count()).toEqual(1); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(Polling.stop).toHaveBeenCalled(); + done(); + }, 100); + + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + }); + }); + + describe('restart', () => { + it('should restart polling when its called', (done) => { + const pollInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); + }; + + Vue.http.interceptors.push(pollInterceptor); + + const service = new ServiceMock('endpoint'); + + spyOn(service, 'fetch').and.callThrough(); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => { + Polling.stop(); + setTimeout(() => { + Polling.restart(); + }, 0); + }, + errorCallback: callbacks.error, + }); + + spyOn(Polling, 'stop').and.callThrough(); + + Polling.makeRequest(); + + setTimeout(() => { + expect(service.fetch.calls.count()).toEqual(2); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(Polling.stop).toHaveBeenCalled(); + done(); + }, 10); + + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + }); + }); +}); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7506e6ab49e..7b9632be84e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -38,6 +38,10 @@ require('vendor/jquery.scrollTo'); } }); + afterEach(function () { + this.class.destroy(); + }); + describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); @@ -200,6 +204,42 @@ require('vendor/jquery.scrollTo'); expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); }); + + describe('#tabShown', () => { + beforeEach(function () { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + }); + + describe('with "Side-by-side"/parallel diff view', () => { + beforeEach(function () { + this.class.diffViewType = () => 'parallel'; + }); + + it('maintains `container-limited` for pipelines tab', function (done) { + const asyncClick = function (selector) { + return new Promise((resolve) => { + setTimeout(() => { + document.querySelector(selector).click(); + resolve(); + }); + }); + }; + + asyncClick('.merge-request-tabs .pipelines-tab a') + .then(() => asyncClick('.merge-request-tabs .diffs-tab a')) + .then(() => asyncClick('.merge-request-tabs .pipelines-tab a')) + .then(() => { + const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); + expect(hasContainerLimitedClass).toBe(true); + }) + .then(done) + .catch((err) => { + done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); + }); + }); + }); + }); + describe('#loadDiff', function () { it('requires an absolute pathname', function () { spyOn($, 'ajax').and.callFake(function (options) { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 285b7940174..f2072a6f350 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -74,9 +74,15 @@ import '~/right_sidebar'; var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); - $('.js-issuable-todo').click(); + $('.issuable-sidebar-header .js-issuable-todo').click(); expect(todoToggleSpy.calls.count()).toEqual(1); }); + + it('should not hide collapsed icons', () => { + [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => { + expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy(); + }); + }); }); }).call(window); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index c12b44cea89..b30c5da8822 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -8,9 +8,6 @@ jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; require('~/commons/index.js'); window.$ = window.jQuery = require('jquery'); window._ = require('underscore'); -window.Cookies = require('js-cookie'); -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); // stub expected globals window.gl = window.gl || {}; @@ -32,37 +29,60 @@ testsContext.keys().forEach(function (path) { } }); -// workaround: include all source files to find files with 0% coverage -// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 -describe('Uncovered files', function () { - // the following files throw errors because of undefined variables +// if we're generating coverage reports, make sure to include all files so +// that we can catch files with 0% coverage +// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 +if (process.env.BABEL_ENV === 'coverage') { + // exempt these files from the coverage report const troubleMakers = [ - './blob_edit/blob_edit_bundle.js', + './blob_edit/blob_bundle.js', + './boards/boards_bundle.js', + './cycle_analytics/cycle_analytics_bundle.js', './cycle_analytics/components/stage_plan_component.js', './cycle_analytics/components/stage_staging_component.js', './cycle_analytics/components/stage_test_component.js', + './commit/pipelines/pipelines_bundle.js', + './diff_notes/diff_notes_bundle.js', './diff_notes/components/jump_to_discussion.js', './diff_notes/components/resolve_count.js', + './dispatcher.js', + './environments/environments_bundle.js', + './filtered_search/filtered_search_bundle.js', + './graphs/graphs_bundle.js', + './issuable/issuable_bundle.js', + './issuable/time_tracking/time_tracking_bundle.js', + './main.js', + './merge_conflicts/merge_conflicts_bundle.js', './merge_conflicts/components/inline_conflict_lines.js', './merge_conflicts/components/parallel_conflict_lines.js', + './merge_request_widget/ci_bundle.js', + './monitoring/monitoring_bundle.js', + './network/network_bundle.js', './network/branch_graph.js', + './profile/profile_bundle.js', + './protected_branches/protected_branches_bundle.js', + './snippet/snippet_bundle.js', + './terminal/terminal_bundle.js', + './users/users_bundle.js', ]; - const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.js$/); - sourceFiles.keys().forEach(function (path) { - // ignore if there is a matching spec file - if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { - return; - } + describe('Uncovered files', function () { + const sourceFiles = require.context('~', true, /\.js$/); + sourceFiles.keys().forEach(function (path) { + // ignore if there is a matching spec file + if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { + return; + } - it(`includes '${path}'`, function () { - try { - sourceFiles(path); - } catch (err) { - if (troubleMakers.indexOf(path) === -1) { - expect(err).toBeNull(); + it(`includes '${path}'`, function () { + try { + sourceFiles(path); + } catch (err) { + if (troubleMakers.indexOf(path) === -1) { + expect(err).toBeNull(); + } } - } + }); }); }); -}); +} diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index 205e72af600..c0375ebc61c 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -1,57 +1,37 @@ -const UserCallout = require('~/user_callout'); +import Cookies from 'js-cookie'; +import UserCallout from '~/user_callout'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; -const Cookie = window.Cookies; describe('UserCallout', function () { - const fixtureName = 'static/user_callout.html.raw'; + const fixtureName = 'dashboard/user-callout.html.raw'; preloadFixtures(fixtureName); beforeEach(() => { loadFixtures(fixtureName); - Cookie.remove(USER_CALLOUT_COOKIE); + Cookies.remove(USER_CALLOUT_COOKIE); this.userCallout = new UserCallout(); - this.closeButton = $('.close-user-callout'); - this.userCalloutBtn = $('.user-callout-btn'); + this.closeButton = $('.js-close-callout.close'); + this.userCalloutBtn = $('.js-close-callout:not(.close)'); this.userCalloutContainer = $('.user-callout'); }); - it('does not show when cookie is set not defined', () => { - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined(); - expect(this.userCalloutContainer.is(':visible')).toBe(true); - }); - - it('shows when cookie is set to false', () => { - Cookie.set(USER_CALLOUT_COOKIE, 'false'); + it('hides when user clicks on the dismiss-icon', (done) => { + this.closeButton.click(); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); - expect(this.userCalloutContainer.is(':visible')).toBe(true); - }); + setTimeout(() => { + expect( + document.querySelector('.user-callout'), + ).toBeNull(); - it('hides when user clicks on the dismiss-icon', () => { - this.closeButton.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + done(); + }); }); it('hides when user clicks on the "check it out" button', () => { this.userCalloutBtn.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); - }); -}); - -describe('UserCallout when cookie is present', function () { - const fixtureName = 'static/user_callout.html.raw'; - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - Cookie.set(USER_CALLOUT_COOKIE, 'true'); - this.userCallout = new UserCallout(); - this.userCalloutContainer = $('.user-callout'); - }); - - it('removes the DOM element', () => { - expect(this.userCalloutContainer.length).toBe(0); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); }); }); diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js new file mode 100644 index 00000000000..bc8e504c413 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import asyncButtonComp from '~/vue_pipelines_index/components/async_button'; + +describe('Pipelines Async Button', () => { + let component; + let spy; + let AsyncButtonComponent; + + beforeEach(() => { + AsyncButtonComponent = Vue.extend(asyncButtonComp); + + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + service: { + postAction: spy, + }, + }, + }).$mount(); + }); + + it('should render a button', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + }); + + it('should render the provided icon', () => { + expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + }); + + it('should render the provided title', () => { + expect(component.$el.getAttribute('title')).toContain('Foo'); + expect(component.$el.getAttribute('aria-label')).toContain('Foo'); + }); + + it('should render the provided cssClass', () => { + expect(component.$el.getAttribute('class')).toContain('bar'); + }); + + it('should call the service when it is clicked with the provided endpoint', () => { + component.$el.click(); + expect(spy).toHaveBeenCalledWith('/foo'); + }); + + it('should hide loading if request fails', () => { + spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + dataAttributes: { + 'data-foo': 'foo', + }, + service: { + postAction: spy, + }, + }, + }).$mount(); + + component.$el.click(); + expect(component.$el.querySelector('.fa-spinner')).toBe(null); + }); + + describe('With confirm dialog', () => { + it('should call the service when confimation is positive', () => { + spyOn(window, 'confirm').and.returnValue(true); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + service: { + postAction: spy, + }, + confirmActionMessage: 'bar', + }, + }).$mount(); + + component.$el.click(); + expect(spy).toHaveBeenCalledWith('/foo'); + }); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js new file mode 100644 index 00000000000..733337168dc --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import emptyStateComp from '~/vue_pipelines_index/components/empty_state'; + +describe('Pipelines Empty State', () => { + let component; + let EmptyStateComponent; + + beforeEach(() => { + EmptyStateComponent = Vue.extend(emptyStateComp); + + component = new EmptyStateComponent({ + propsData: { + helpPagePath: 'foo', + }, + }).$mount(); + }); + + it('should render empty state SVG', () => { + expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); + }); + + it('should render emtpy state information', () => { + expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); + + expect( + component.$el.querySelector('p').textContent, + ).toContain('Continous Integration can help catch bugs by running your tests automatically'); + + expect( + component.$el.querySelector('p').textContent, + ).toContain('Continuous Deployment can help you deliver code to your product environment'); + }); + + it('should render a link with provided help path', () => { + expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js new file mode 100644 index 00000000000..524e018b1fa --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import errorStateComp from '~/vue_pipelines_index/components/error_state'; + +describe('Pipelines Error State', () => { + let component; + let ErrorStateComponent; + + beforeEach(() => { + ErrorStateComponent = Vue.extend(errorStateComp); + + component = new ErrorStateComponent().$mount(); + }); + + it('should render error state SVG', () => { + expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); + }); + + it('should render emtpy state information', () => { + expect( + component.$el.querySelector('h4').textContent, + ).toContain('The API failed to fetch the pipelines'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/vue_pipelines_index/mock_data.js new file mode 100644 index 00000000000..2365a662b9f --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/mock_data.js @@ -0,0 +1,107 @@ +export default { + pipelines: [{ + id: 115, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + path: '/root/review-app/pipelines/115', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/115', + }, + duration: null, + finished_at: '2017-03-17T19:00:15.996Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/115#build', + }, + path: '/root/review-app/pipelines/115#build', + dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build', + }, + { + name: 'review', + title: 'review: skipped', + status: { + icon: 'icon_status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/root/review-app/pipelines/115#review', + }, + path: '/root/review-app/pipelines/115#review', + dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review', + }], + artifacts: [], + manual_actions: [{ + name: 'stop_review', + path: '/root/review-app/builds/3766/play', + }], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: { + name: 'thisisabranch', + path: '/root/review-app/tree/thisisabranch', + tag: false, + branch: true, + }, + commit: { + id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600', + short_id: '9e87f876', + title: 'Update README.md', + created_at: '2017-03-15T22:58:28.000+00:00', + parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'], + message: 'Update README.md', + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-03-15T22:58:28.000+00:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-03-15T22:58:28.000+00:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600', + commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600', + }, + retry_path: '/root/review-app/pipelines/115/retry', + created_at: '2017-03-15T22:58:33.436Z', + updated_at: '2017-03-17T19:00:15.997Z', + }], + count: { + all: 52, + running: 0, + pending: 0, + finished: 52, + }, +}; diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js new file mode 100644 index 00000000000..659c4854a56 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import navControlsComp from '~/vue_pipelines_index/components/nav_controls'; + +describe('Pipelines Nav Controls', () => { + let NavControlsComponent; + + beforeEach(() => { + NavControlsComponent = Vue.extend(navControlsComp); + }); + + it('should render link to create a new pipeline', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); + expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); + }); + + it('should not render link to create pipeline if no permission is provided', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: false, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-create')).toEqual(null); + }); + + it('should render link for CI lint', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint'); + expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath); + }); + + it('should render link to help page when CI is not enabled', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: false, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath); + }); + + it('should not render link to help page when CI is enabled', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-info')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js new file mode 100644 index 00000000000..96a2a37b5f7 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url'; + +describe('Pipeline Url Component', () => { + let PipelineUrlComponent; + + beforeEach(() => { + PipelineUrlComponent = Vue.extend(pipelineUrlComp); + }); + + it('should render a table cell', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.tagName).toEqual('TD'); + }); + + it('should render a link the provided path and id', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1'); + }); + + it('should render user information when a user is provided', () => { + const mockData = { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + user: { + web_url: '/', + name: 'foo', + avatar_url: '/', + }, + }, + }; + + const component = new PipelineUrlComponent({ + propsData: mockData, + }).$mount(); + + const image = component.$el.querySelector('.js-pipeline-url-user img'); + + expect( + component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), + ).toEqual(mockData.pipeline.user.web_url); + expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name); + expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); + }); + + it('should render "API" when no user is provided', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API'); + }); + + it('should render latest, yaml invalid and stuck flags when provided', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: { + latest: true, + yaml_errors: true, + stuck: true, + }, + }, + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest'); + expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); + expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js new file mode 100644 index 00000000000..dba998c7688 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions'; + +describe('Pipelines Actions dropdown', () => { + let component; + let spy; + let actions; + let ActionsComponent; + + beforeEach(() => { + ActionsComponent = Vue.extend(pipelinesActionsComp); + + actions = [ + { + name: 'stop_review', + path: '/root/review-app/builds/1893/play', + }, + ]; + + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + + component = new ActionsComponent({ + propsData: { + actions, + service: { + postAction: spy, + }, + }, + }).$mount(); + }); + + it('should render a dropdown with the provided actions', () => { + expect( + component.$el.querySelectorAll('.dropdown-menu li').length, + ).toEqual(actions.length); + }); + + it('should call the service when an action is clicked', () => { + component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); + component.$el.querySelector('.js-pipeline-action-link').click(); + + expect(spy).toHaveBeenCalledWith(actions[0].path); + }); + + it('should hide loading if request fails', () => { + spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); + + component = new ActionsComponent({ + propsData: { + actions, + service: { + postAction: spy, + }, + }, + }).$mount(); + + component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); + component.$el.querySelector('.js-pipeline-action-link').click(); + + expect(component.$el.querySelector('.fa-spinner')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js new file mode 100644 index 00000000000..f7f49649c1c --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts'; + +describe('Pipelines Artifacts dropdown', () => { + let component; + let artifacts; + + beforeEach(() => { + const ArtifactsComponent = Vue.extend(artifactsComp); + + artifacts = [ + { + name: 'artifact', + path: '/download/path', + }, + ]; + + component = new ArtifactsComponent({ + propsData: { + artifacts, + }, + }).$mount(); + }); + + it('should render a dropdown with the provided artifacts', () => { + expect( + component.$el.querySelectorAll('.dropdown-menu li').length, + ).toEqual(artifacts.length); + }); + + it('should render a link with the provided path', () => { + expect( + component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), + ).toEqual(artifacts[0].path); + + expect( + component.$el.querySelector('.dropdown-menu li a span').textContent, + ).toContain(artifacts[0].name); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_spec.js new file mode 100644 index 00000000000..725f6cb2d7a --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_spec.js @@ -0,0 +1,114 @@ +import Vue from 'vue'; +import pipelinesComp from '~/vue_pipelines_index/pipelines'; +import Store from '~/vue_pipelines_index/stores/pipelines_store'; +import pipelinesData from './mock_data'; + +describe('Pipelines', () => { + preloadFixtures('static/pipelines.html.raw'); + + let PipelinesComponent; + + beforeEach(() => { + loadFixtures('static/pipelines.html.raw'); + + PipelinesComponent = Vue.extend(pipelinesComp); + }); + + describe('successfull request', () => { + describe('with pipelines', () => { + const pipelinesInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(pipelinesData), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('should render table', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.table-holder')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); + + describe('without pipelines', () => { + const emptyInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(emptyInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyInterceptor, + ); + }); + + it('should render empty state', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.empty-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); + }); + + describe('unsuccessfull request', () => { + const errorInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(errorInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('should render error state', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js new file mode 100644 index 00000000000..5c0934404bb --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js @@ -0,0 +1,72 @@ +import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store'; + +describe('Pipelines Store', () => { + let store; + + beforeEach(() => { + store = new PipelineStore(); + }); + + it('should be initialized with an empty state', () => { + expect(store.state.pipelines).toEqual([]); + expect(store.state.count).toEqual({}); + expect(store.state.pageInfo).toEqual({}); + }); + + describe('storePipelines', () => { + it('should use the default parameter if none is provided', () => { + store.storePipelines(); + expect(store.state.pipelines).toEqual([]); + }); + + it('should store the provided array', () => { + const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }]; + store.storePipelines(array); + expect(store.state.pipelines).toEqual(array); + }); + }); + + describe('storeCount', () => { + it('should use the default parameter if none is provided', () => { + store.storeCount(); + expect(store.state.count).toEqual({}); + }); + + it('should store the provided count', () => { + const count = { all: 20, finished: 10 }; + store.storeCount(count); + + expect(store.state.count).toEqual(count); + }); + }); + + describe('storePagination', () => { + it('should use the default parameter if none is provided', () => { + store.storePagination(); + expect(store.state.pageInfo).toEqual({}); + }); + + it('should store pagination information normalized and parsed', () => { + const pagination = { + 'X-nExt-pAge': '2', + 'X-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '2', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }; + + const expectedResult = { + perPage: 1, + page: 1, + total: 37, + totalPages: 2, + nextPage: 2, + previousPage: 2, + }; + + store.storePagination(pagination); + expect(store.state.pageInfo).toEqual(expectedResult); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 15ab10b9b69..df547299d75 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -1,13 +1,17 @@ -require('~/vue_shared/components/commit'); +import Vue from 'vue'; +import commitComp from '~/vue_shared/components/commit'; describe('Commit component', () => { let props; let component; + let CommitComponent; + + beforeEach(() => { + CommitComponent = Vue.extend(commitComp); + }); it('should render a code-fork icon if it does not represent a tag', () => { - setFixtures('<div class="test-commit-container"></div>'); - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: { tag: false, commitRef: { @@ -23,15 +27,13 @@ describe('Commit component', () => { username: 'jschatz1', }, }, - }); + }).$mount(); expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork'); }); describe('Given all the props', () => { beforeEach(() => { - setFixtures('<div class="test-commit-container"></div>'); - props = { tag: true, commitRef: { @@ -49,10 +51,9 @@ describe('Commit component', () => { commitIconSvg: '<svg></svg>', }; - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: props, - }); + }).$mount(); }); it('should render a tag icon if it represents a tag', () => { @@ -105,7 +106,6 @@ describe('Commit component', () => { describe('When commit title is not provided', () => { it('should render default message', () => { - setFixtures('<div class="test-commit-container"></div>'); props = { tag: false, commitRef: { @@ -118,10 +118,9 @@ describe('Commit component', () => { author: {}, }; - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), + component = new CommitComponent({ propsData: props, - }); + }).$mount(); expect( component.$el.querySelector('.commit-title span').textContent, diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 412abfd5e41..699625cdbb7 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -1,20 +1,20 @@ -require('~/vue_shared/components/pipelines_table_row'); -const pipeline = require('../../commit/pipelines/mock_data'); +import Vue from 'vue'; +import tableRowComp from '~/vue_shared/components/pipelines_table_row'; +import pipeline from '../../commit/pipelines/mock_data'; describe('Pipelines Table Row', () => { let component; - preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + const PipelinesTableRowComponent = Vue.extend(tableRowComp); - component = new gl.pipelines.PipelinesTableRowComponent({ + component = new PipelinesTableRowComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipeline, - svgs: {}, + service: {}, }, - }); + }).$mount(); }); it('should render a table row', () => { diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js index 54d81e2ea7d..4d3ced944d7 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js @@ -1,24 +1,28 @@ -require('~/vue_shared/components/pipelines_table'); -require('~/lib/utils/datetime_utility'); -const pipeline = require('../../commit/pipelines/mock_data'); +import Vue from 'vue'; +import pipelinesTableComp from '~/vue_shared/components/pipelines_table'; +import '~/lib/utils/datetime_utility'; +import pipeline from '../../commit/pipelines/mock_data'; describe('Pipelines Table', () => { - preloadFixtures('static/environments/element.html.raw'); + let PipelinesTableComponent; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + PipelinesTableComponent = Vue.extend(pipelinesTableComp); }); describe('table', () => { let component; beforeEach(() => { - component = new gl.pipelines.PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), + component = new PipelinesTableComponent({ propsData: { pipelines: [], - svgs: {}, + service: {}, }, - }); + }).$mount(); + }); + + afterEach(() => { + component.$destroy(); }); it('should render a table', () => { @@ -37,26 +41,25 @@ describe('Pipelines Table', () => { describe('without data', () => { it('should render an empty table', () => { - const component = new gl.pipelines.PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), + const component = new PipelinesTableComponent({ propsData: { pipelines: [], - svgs: {}, + service: {}, }, - }); + }).$mount(); expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); }); }); describe('with data', () => { it('should render rows', () => { - const component = new gl.pipelines.PipelinesTableComponent({ + const component = new PipelinesTableComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], - svgs: {}, + service: {}, }, - }); + }).$mount(); expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); }); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 9cb067921a7..96038718191 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -1,8 +1,10 @@ -require('~/lib/utils/common_utils'); -require('~/vue_shared/components/table_pagination'); +import Vue from 'vue'; +import paginationComp from '~/vue_shared/components/table_pagination'; +import '~/lib/utils/common_utils'; describe('Pagination component', () => { let component; + let PaginationComponent; const changeChanges = { one: '', @@ -12,11 +14,12 @@ describe('Pagination component', () => { changeChanges.one = one; }; - it('should render and start at page 1', () => { - setFixtures('<div class="test-pagination-container"></div>'); + beforeEach(() => { + PaginationComponent = Vue.extend(paginationComp); + }); - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + it('should render and start at page 1', () => { + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -25,7 +28,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); expect(component.$el.classList).toContain('gl-pagination'); @@ -35,10 +38,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -47,7 +47,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: 'Prev' } }); @@ -55,10 +55,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -67,7 +64,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: 'Next' } }); @@ -75,10 +72,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -87,18 +81,15 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); - component.changePage({ target: { innerText: 'Last >>' } }); + component.changePage({ target: { innerText: 'Last »' } }); expect(changeChanges.one).toEqual(10); }); it('should go to the first page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -107,18 +98,15 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); - component.changePage({ target: { innerText: '<< First' } }); + component.changePage({ target: { innerText: '« First' } }); expect(changeChanges.one).toEqual(1); }); it('should do nothing', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), + component = new PaginationComponent({ propsData: { pageInfo: { totalPages: 10, @@ -127,7 +115,7 @@ describe('Pagination component', () => { }, change, }, - }); + }).$mount(); component.changePage({ target: { innerText: '...' } }); @@ -136,6 +124,10 @@ describe('Pagination component', () => { }); describe('paramHelper', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + it('can parse url parameters correctly', () => { window.history.pushState({}, null, '?scope=all&p=2'); diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 11607d4fb26..f1082495fcc 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -21,6 +21,19 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end + describe 'performance' do + let(:another_issue) { create(:issue, project: project) } + + it 'does not have a N+1 query problem' do + single_reference = "Issue #{issue.to_reference}" + multiple_references = "Issues #{issue.to_reference} and #{another_issue.to_reference}" + + control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count + + expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count) + end + end + context 'internal reference' do it_behaves_like 'a reference containing an element node' diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 3d3d36061f4..40232f6e426 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -17,6 +17,19 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do end end + describe 'performance' do + let(:another_merge) { create(:merge_request, source_project: project, source_branch: 'fix') } + + it 'does not have a N+1 query problem' do + single_reference = "Merge request #{merge.to_reference}" + multiple_references = "Merge requests #{merge.to_reference} and #{another_merge.to_reference}" + + control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count + + expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count) + end + end + context 'internal reference' do let(:reference) { merge.to_reference } diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 9873774909e..63b23dac7ed 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -83,6 +83,14 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(doc.css('a').length).to eq 1 end + it 'links to a User with different case-sensitivity' do + user = create(:user, username: 'RescueRanger') + + doc = reference_filter("Hey #{user.to_reference.upcase}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').text).to eq(user.to_reference) + end + it 'includes a data-user attribute' do doc = reference_filter("Hey #{reference}") link = doc.css('a').first diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb index 2a314a744ca..49457b129e3 100644 --- a/spec/lib/gitlab/ci/build/step_spec.rb +++ b/spec/lib/gitlab/ci/build/step_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::Ci::Build::Step do end context 'when after_script is not empty' do - let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) } + let(:job) { create(:ci_build, options: { after_script: ['ls -la', 'date'] }) } it 'fabricates an object' do expect(subject.name).to eq(:after_script) diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 8b3bd08cf13..e648a3ac3a2 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -27,6 +27,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'passed' expect(status.icon).to eq 'icon_status_success' + expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq 'passed' expect(status).to have_details expect(status).to have_action @@ -53,6 +54,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' expect(status.icon).to eq 'icon_status_failed' + expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' expect(status).to have_details expect(status).to have_action @@ -79,6 +81,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' expect(status.icon).to eq 'icon_status_warning' + expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' expect(status).to have_details expect(status).to have_action @@ -107,6 +110,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'canceled' expect(status.icon).to eq 'icon_status_canceled' + expect(status.favicon).to eq 'favicon_status_canceled' expect(status.label).to eq 'canceled' expect(status).to have_details expect(status).to have_action @@ -132,6 +136,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'running' expect(status.icon).to eq 'icon_status_running' + expect(status.favicon).to eq 'favicon_status_running' expect(status.label).to eq 'running' expect(status).to have_details expect(status).to have_action @@ -157,6 +162,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'pending' expect(status.icon).to eq 'icon_status_pending' + expect(status.favicon).to eq 'favicon_status_pending' expect(status.label).to eq 'pending' expect(status).to have_details expect(status).to have_action @@ -181,6 +187,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'skipped' expect(status.icon).to eq 'icon_status_skipped' + expect(status.favicon).to eq 'favicon_status_skipped' expect(status.label).to eq 'skipped' expect(status).to have_details expect(status).not_to have_action @@ -208,6 +215,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' + expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to eq 'manual play action' expect(status).to have_details expect(status).to have_action @@ -235,6 +243,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' + expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to eq 'manual stop action' expect(status).to have_details expect(status).to have_action diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 768f8926f1d..530639a5897 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Canceled do it { expect(subject.icon).to eq 'icon_status_canceled' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_canceled' } + end + describe '#group' do it { expect(subject.group).to eq 'canceled' } end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index e96c13aede3..aef982e17f1 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Created do it { expect(subject.icon).to eq 'icon_status_created' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_created' } + end + describe '#group' do it { expect(subject.group).to eq 'created' } end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index e5da0a91159..9a25743885c 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Failed do it { expect(subject.icon).to eq 'icon_status_failed' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_failed' } + end + describe '#group' do it { expect(subject.group).to eq 'failed' } end diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb index 3fd3727b92d..6fdc3801d71 100644 --- a/spec/lib/gitlab/ci/status/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Manual do it { expect(subject.icon).to eq 'icon_status_manual' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_manual' } + end + describe '#group' do it { expect(subject.group).to eq 'manual' } end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 8d09cf2a05a..ffc53f0506b 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pending do it { expect(subject.icon).to eq 'icon_status_pending' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_pending' } + end + describe '#group' do it { expect(subject.group).to eq 'pending' } end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 10d3bf749c1..0babf1fb54e 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Running do it { expect(subject.icon).to eq 'icon_status_running' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_running' } + end + describe '#group' do it { expect(subject.group).to eq 'running' } end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index 10db93d3802..670747c9f0b 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Skipped do it { expect(subject.icon).to eq 'icon_status_skipped' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_skipped' } + end + describe '#group' do it { expect(subject.group).to eq 'skipped' } end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 230f24b94a4..ff65b074808 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Success do it { expect(subject.icon).to eq 'icon_status_success' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_success' } + end + describe '#group' do it { expect(subject.group).to eq 'success' } end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index edd01d032c8..4ce4e6e1034 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' -class MigrationTest - include Gitlab::Database -end - describe Gitlab::Database, lib: true do + before do + stub_const('MigrationTest', Class.new { include Gitlab::Database }) + end + + describe '.config' do + it 'returns a Hash' do + expect(described_class.config).to be_an_instance_of(Hash) + end + end + describe '.adapter_name' do it 'returns the name of the adapter' do expect(described_class.adapter_name).to be_an_instance_of(String) diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 8b5bfc4dbb0..6ec4360adc2 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -99,6 +99,19 @@ describe Gitlab::EtagCaching::Middleware do middleware.call(build_env(path, if_none_match)) end + + context 'when polling is disabled' do + before do + allow(Gitlab::PollingInterval).to receive(:polling_enabled?). + and_return(false) + end + + it 'returns status code 429' do + status, _, _ = middleware.call(build_env(path, if_none_match)) + + expect(status).to eq 429 + end + end end context 'when If-None-Match header does not match ETag in store' do diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb index 17d6be470ac..d6d365f6492 100644 --- a/spec/lib/gitlab/git/blob_snippet_spec.rb +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::BlobSnippet, seed_helper: true do - describe :data do + describe '#data' do context 'empty lines' do let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) } diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 8049e2c120d..b883526151e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" describe Gitlab::Git::Blob, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - describe :initialize do + describe 'initialize' do let(:blob) { Gitlab::Git::Blob.new(name: 'test') } it 'handles nil data' do @@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :find do + describe '.find' do context 'file in subdir' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } @@ -101,7 +101,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :raw do + describe '.raw' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } @@ -222,7 +222,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :lfs_pointers do + describe 'lfs_pointers' do context 'file a valid lfs pointer' do let(:blob) do Gitlab::Git::Blob.find( diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index e1be6784c20..5cf4631fbfc 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'Class methods' do - describe :find do + describe '.find' do it "should return first head commit if without params" do expect(Gitlab::Git::Commit.last(repository).id).to eq( repository.raw.head.target.oid @@ -103,7 +103,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :last_for_path do + describe '.last_for_path' do context 'no path' do subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } @@ -132,7 +132,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe "where" do + describe '.where' do context 'path is empty string' do subject do commits = Gitlab::Git::Commit.where( @@ -230,7 +230,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :between do + describe '.between' do subject do commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) commits.map { |c| c.id } @@ -243,7 +243,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { is_expected.not_to include(SeedRepo::FirstCommit::ID) } end - describe :find_all do + describe '.find_all' do context 'max_count' do subject do commits = Gitlab::Git::Commit.find_all( @@ -304,7 +304,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :init_from_rugged do + describe '#init_from_rugged' do let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } subject { gitlab_commit } @@ -314,7 +314,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :init_from_hash do + describe '#init_from_hash' do let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } subject { commit } @@ -329,7 +329,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :stats do + describe '#stats' do subject { commit.stats } describe '#additions' do @@ -343,25 +343,25 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :to_diff do + describe '#to_diff' do subject { commit.to_diff } it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} end - describe :has_zero_stats? do + describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end - describe :to_patch do + describe '#to_patch' do subject { commit.to_patch } it { is_expected.to include "From #{SeedRepo::Commit::ID}" } it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} end - describe :to_hash do + describe '#to_hash' do let(:hash) { commit.to_hash } subject { hash } @@ -373,7 +373,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :diffs do + describe '#diffs' do subject { commit.diffs } it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } @@ -381,7 +381,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(subject.first).to be_kind_of Gitlab::Git::Diff } end - describe :ref_names do + describe '#ref_names' do let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } subject { commit.ref_names(repository) } diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index f66b68e4218..e28debe1494 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Git::Compare, seed_helper: true do let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } - describe :commits do + describe '#commits' do subject do compare.commits.map(&:id) end @@ -42,7 +42,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :diffs do + describe '#diffs' do subject do compare.diffs.map(&:new_path) end @@ -67,7 +67,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :same do + describe '#same' do subject do compare.same end @@ -81,7 +81,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :commits_straight do + describe '#commits', 'straight compare' do subject do compare_straight.commits.map(&:id) end @@ -94,7 +94,7 @@ describe Gitlab::Git::Compare, seed_helper: true do it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } end - describe :diffs_straight do + describe '#diffs', 'straight compare' do subject do compare_straight.diffs.map(&:new_path) end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 47bdd7310d5..122c93dcd69 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { is_expected.to be_kind_of ::Array } end - describe :decorate! do + describe '#decorate!' do let(:file_count) { 3 } it 'modifies the array in place' do @@ -302,7 +302,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end end - describe :each do + describe '#each' do context 'when diff are too large' do let(:collection) do Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }]) diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4c55532d165..992126ef153 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -109,6 +109,43 @@ EOT end end end + + context 'using a Gitaly::CommitDiffResponse' do + let(:diff) do + described_class.new( + Gitaly::CommitDiffResponse.new( + to_path: ".gitmodules", + from_path: ".gitmodules", + old_mode: 0100644, + new_mode: 0100644, + from_id: '357406f3075a57708d0163752905cc1576fceacc', + to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', + raw_chunks: raw_chunks, + ) + ) + end + + context 'with a small diff' do + let(:raw_chunks) { [@raw_diff_hash[:diff]] } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + let(:raw_chunks) { ['a' * 204800] } + + it 'prunes the diff' do + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + end end describe 'straight diffs' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9ed43da1116..d4b7684adfd 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -771,8 +771,8 @@ describe Gitlab::Git::Repository, seed_helper: true do commits = repository.log(options) expect(commits.size).to be > 0 - satisfy do - commits.all? { |commit| commit.created_at >= options[:after] } + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.time >= options[:after] } end end end @@ -784,8 +784,27 @@ describe Gitlab::Git::Repository, seed_helper: true do commits = repository.log(options) expect(commits.size).to be > 0 - satisfy do - commits.all? { |commit| commit.created_at <= options[:before] } + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.time <= options[:before] } + end + end + end + + context 'when multiple paths are provided' do + let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } + + def commit_files(commit) + commit.diff(commit.parent_ids.first).deltas.flat_map do |delta| + [delta.old_file[:path], delta.new_file[:path]].uniq.compact + end + end + + it 'only returns commits matching at least one path' do + commits = repository.log(options) + + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } end end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 688e2a75373..82685712b5b 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(tree.select(&:file?).size).to eq(10) } it { expect(tree.select(&:submodule?).size).to eq(2) } - describe :dir do + describe '#dir?' do let(:dir) { tree.select(&:dir?).first } it { expect(dir).to be_kind_of Gitlab::Git::Tree } @@ -19,6 +19,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(dir.name).to eq('encoding') } it { expect(dir.path).to eq('encoding') } + it { expect(dir.mode).to eq('40000') } context :subdir do let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } @@ -41,7 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do end end - describe :file do + describe '#file?' do let(:file) { tree.select(&:file?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } @@ -50,21 +51,21 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(file.name).to eq('.gitignore') } end - describe :readme do + describe '#readme?' do let(:file) { tree.select(&:readme?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } it { expect(file.name).to eq('README.md') } end - describe :contributing do + describe '#contributing?' do let(:file) { tree.select(&:contributing?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } it { expect(file.name).to eq('CONTRIBUTING.md') } end - describe :submodule do + describe '#submodule?' do let(:submodule) { tree.select(&:submodule?).first } it { expect(submodule).to be_kind_of Gitlab::Git::Tree } diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index 8d43b570e98..bcca4d4c746 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::Util do - describe :count_lines do + describe '#count_lines' do [ ["", 0], ["foo", 1], diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb new file mode 100644 index 00000000000..4684b1d1ac0 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Commit do + describe '.diff_from_parent' do + let(:diff_stub) { double('Gitaly::Diff::Stub') } + let(:project) { create(:project, :repository) } + let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) } + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + + before do + allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub) + allow(diff_stub).to receive(:commit_diff).and_return([]) + end + + context 'when a commit has a parent' do + it 'sends an RPC request with the parent ID as left commit' do + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + right_commit_id: commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(commit) + end + end + + context 'when a commit does not have a parent' do + it 'sends an RPC request with empty tree ref as left commit' do + initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + right_commit_id: initial_commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(initial_commit) + end + end + + it 'returns a Gitlab::Git::DiffCollection' do + ret = described_class.diff_from_parent(commit) + + expect(ret).to be_kind_of(Gitlab::Git::DiffCollection) + end + + it 'passes options to Gitlab::Git::DiffCollection' do + options = { max_files: 31, max_lines: 13 } + + expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options) + + described_class.diff_from_parent(commit, options) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb index a6252c99aa1..bb5d93994ad 100644 --- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -1,20 +1,13 @@ require 'spec_helper' describe Gitlab::GitalyClient::Notifications do - let(:client) { Gitlab::GitalyClient::Notifications.new } - - before do - allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') - end - describe '#post_receive' do - let(:repo_path) { '/path/to/my_repo.git' } - it 'sends a post_receive message' do + repo_path = create(:empty_project).repository.path_to_repo expect_any_instance_of(Gitaly::Notifications::Stub). to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path)) - client.post_receive(repo_path) + described_class.new(repo_path).post_receive end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 8b867fbe322..9d5e20841b5 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -215,9 +215,9 @@ describe Gitlab::GithubImport::Importer, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha, user: octocat) } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) } let(:pull_request) do double( number: 1347, diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb index 10449ef5fcb..565435824fd 100644 --- a/spec/lib/gitlab/github_import/label_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::GithubImport::LabelFormatter, lib: true do context 'when label exists' do it 'does not create a new label' do - project.labels.create(name: raw.name) + Labels::CreateService.new(name: raw.name).execute(project: project) expect { subject.create! }.not_to change(Label, :count) end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 44423917944..9b9f7e4d34e 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,15 +4,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) } + let(:target_sha) { target_commit.id } + let(:target_short_sha) { target_commit.id.to_s[0..7] } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) } let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } - let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } - let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) } + let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -203,16 +206,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when source branch does not exist' do let(:raw_data) { double(base_data.merge(head: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch" end end context 'when source branch is from a fork' do let(:raw_data) { double(base_data.merge(head: forked_branch)) } - it 'prefixes branch name with pull request number and project with namespace to avoid collision' do - expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" + end + end + + context 'when source branch is from a deleted fork' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" end end end @@ -229,8 +240,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when target branch does not exist' do let(:raw_data) { double(base_data.merge(base: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch' end end end @@ -290,6 +301,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when source repository does not exist anymore' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + context 'when source and target repositories are the same' do let(:raw_data) { double(base_data.merge(head: source_branch)) } @@ -299,6 +318,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#source_branch_exists?' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns false when is a cross_project' do + expect(pull_request.source_branch_exists?).to eq false + end + end + describe '#url' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e47956a365f..002cffd3062 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -29,6 +29,7 @@ notes: - resolved_by - todos - events +- system_note_metadata label_links: - target - label @@ -130,7 +131,6 @@ project: - campfire_service - drone_ci_service - emails_on_push_service -- builds_email_service - pipelines_email_service - mattermost_slash_commands_service - slack_slash_commands_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index c3d5c451a3c..d9b67426818 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6507,7 +6507,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6565,7 +6564,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6603,7 +6601,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" }, @@ -6624,7 +6621,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6659,7 +6655,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" }, @@ -6695,7 +6690,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6716,7 +6710,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6751,7 +6744,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" }, @@ -6787,7 +6779,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" }, @@ -6808,7 +6799,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6843,7 +6833,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" }, @@ -6879,7 +6868,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" }, @@ -6900,7 +6888,6 @@ "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 5, "status": "failed", "started_at": null, "finished_at": null, @@ -6935,7 +6922,6 @@ "artifacts_file": { "url": null }, - "gl_project_id": 5, "artifacts_metadata": { "url": null }, @@ -6971,7 +6957,6 @@ "artifacts_file": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" }, - "gl_project_id": 5, "artifacts_metadata": { "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" }, @@ -6985,11 +6970,10 @@ { "id": 123, "token": "cdbfasdf44a5958c83654733449e585", - "project_id": null, + "project_id": 5, "deleted_at": null, "created_at": "2017-01-16T15:25:28.637Z", - "updated_at": "2017-01-16T15:25:28.637Z", - "gl_project_id": 123 + "updated_at": "2017-01-16T15:25:28.637Z" } ], "deploy_keys": [ @@ -7047,7 +7031,7 @@ "updated_at": "2016-06-14T15:01:51.303Z", "active": false, "properties": { - "notify_only_broken_builds": true + "notify_only_broken_pipelines": true }, "template": false, "push_events": true, @@ -7055,7 +7039,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "pipeline_events": true, "category": "common", "default": false, "wiki_page_events": true @@ -7174,7 +7158,7 @@ "updated_at": "2016-06-14T15:01:51.219Z", "active": false, "properties": { - "notify_only_broken_builds": true + "notify_only_broken_pipelines": true }, "template": false, "push_events": true, @@ -7182,7 +7166,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "pipeline_events": true, "category": "common", "default": false, "wiki_page_events": true @@ -7335,27 +7319,6 @@ "wiki_page_events": true }, { - "id": 85, - "title": "Builds emails", - "project_id": 5, - "created_at": "2016-06-14T15:01:51.090Z", - "updated_at": "2016-06-14T15:01:51.090Z", - "active": false, - "properties": { - "notify_only_broken_builds": true - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "build_events": true, - "category": "common", - "default": false, - "wiki_page_events": true - }, - { "id": 84, "title": "Buildkite", "project_id": 5, @@ -7503,4 +7466,4 @@ "updated_at": "2016-09-23T11:58:28.000Z", "wiki_access_level": 20 } -}
\ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index f4a21c24fa1..c36f12dbd82 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Ci::Build.where(token: 'abcd')).to be_empty end end + + context 'has restored the correct number of records' do + it 'has the correct number of merge requests' do + expect(@project.merge_requests.size).to eq(9) + end + + it 'has the correct number of triggers' do + expect(@project.triggers.size).to eq(1) + end + + it 'has the correct number of pipelines and statuses' do + expect(@project.pipelines.size).to eq(5) + + @project.pipelines.zip([2, 2, 2, 2, 2]) + .each do |(pipeline, expected_status_size)| + expect(pipeline.statuses.size).to eq(expected_status_size) + end + end + end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index c718e792461..1ad16a9b57d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -15,6 +15,7 @@ Issue: - updated_by_id - confidential - deleted_at +- closed_at - due_date - moved_to_id - lock_version @@ -176,7 +177,6 @@ Ci::Pipeline: - tag - yaml_errors - committed_at -- gl_project_id - status - started_at - finished_at @@ -211,7 +211,6 @@ CommitStatus: - target_url - description - artifacts_file -- gl_project_id - artifacts_metadata - erased_by_id - erased_at @@ -232,7 +231,6 @@ Ci::Variable: - encrypted_value - encrypted_value_salt - encrypted_value_iv -- gl_project_id Ci::Trigger: - id - token @@ -240,7 +238,6 @@ Ci::Trigger: - deleted_at - created_at - updated_at -- gl_project_id - owner_id - description DeployKey: diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 2f3bd4393b7..346cf0d117c 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -57,7 +57,7 @@ describe Gitlab::LDAP::User, lib: true do end end - describe :find_or_create do + describe 'find or create' do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') diff --git a/spec/lib/gitlab/polling_interval_spec.rb b/spec/lib/gitlab/polling_interval_spec.rb new file mode 100644 index 00000000000..56c2847e26a --- /dev/null +++ b/spec/lib/gitlab/polling_interval_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::PollingInterval, lib: true do + let(:polling_interval) { described_class } + + describe '.set_header' do + let(:headers) { {} } + let(:response) { double(headers: headers) } + + context 'when polling is disabled' do + before do + stub_application_setting(polling_interval_multiplier: 0) + end + + it 'sets value to -1' do + polling_interval.set_header(response, interval: 10_000) + + expect(headers['Poll-Interval']).to eq(-1) + end + end + + context 'when polling is enabled' do + before do + stub_application_setting(polling_interval_multiplier: 0.33333) + end + + it 'applies modifier to base interval' do + polling_interval.set_header(response, interval: 10_000) + + expect(headers['Poll-Interval']).to eq(3333) + end + end + end +end diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb new file mode 100644 index 00000000000..0fb5d7646f2 --- /dev/null +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe ::Gitlab::RepoPath do + describe '.strip_storage_path' do + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'storage1' => { 'path' => '/foo' }, + 'storage2' => { 'path' => '/bar' }, + }) + end + + it 'strips the storage path' do + expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git') + end + + it 'raises NotFoundError if no storage matches the path' do + expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error( + described_class::NotFoundError + ) + end + end +end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb new file mode 100644 index 00000000000..a504d299307 --- /dev/null +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::UrlBlocker, lib: true do + describe '#blocked_url?' do + it 'allows imports from configured web host and port' do + import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git" + expect(described_class.blocked_url?(import_url)).to be false + end + + it 'allows imports from configured SSH host and port' do + import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" + expect(described_class.blocked_url?(import_url)).to be false + end + + it 'returns true for bad localhost hostname' do + expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true + end + + it 'returns true for bad port' do + expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true + end + + it 'returns true for invalid URL' do + expect(described_class.blocked_url?('http://:8080')).to be true + end + + it 'returns false for legitimate URL' do + expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false + end + end +end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 3fd361de458..fc144a2556a 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do let(:url_sanitizer) do described_class.new("https://github.com/me/project.git", credentials: credentials) end + let(:user) { double(:user, username: 'john.doe') } describe '.sanitize' do def sanitize_url(url) @@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do end end + describe '.valid?' do + it 'validates url strings' do + expect(described_class.valid?(nil)).to be(false) + expect(described_class.valid?('valid@project:url.git')).to be(true) + expect(described_class.valid?('123://invalid:url')).to be(false) + end + end + + describe '.http_credentials_for_user' do + it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) } + it { expect(described_class.http_credentials_for_user('foo')).to eq({}) } + end + describe '#sanitized_url' do it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } end describe '#credentials' do it { expect(url_sanitizer.credentials).to eq(credentials) } + + context 'when user is given to #initialize' do + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user)) + end + + it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) } + end end describe '#full_url' do @@ -69,13 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do expect(sanitizer.full_url).to eq('user@server:project.git') end - end - describe '.valid?' do - it 'validates url strings' do - expect(described_class.valid?(nil)).to be(false) - expect(described_class.valid?('valid@project:url.git')).to be(true) - expect(described_class.valid?('123://invalid:url')).to be(false) + context 'when user is given to #initialize' do + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user)) + end + + it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") } end end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb new file mode 100644 index 00000000000..3255c6f1ef7 --- /dev/null +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::VisibilityLevel, lib: true do + describe '.level_value' do + it 'converts "public" to integer value' do + expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'converts string integer to integer value' do + expect(described_class.level_value('20')).to eq(20) + end + + it 'defaults to PRIVATE when string value is not valid' do + expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'defaults to PRIVATE when integer value is not valid' do + expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 8e5e8288c49..cb7c810124e 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -179,22 +179,24 @@ describe Gitlab::Workhorse, lib: true do describe '.git_http_ok' do let(:user) { create(:user) } + let(:repo_path) { repository.path_to_repo } subject { described_class.git_http_ok(repository, user) } - it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) } - - context 'when Gitaly socket path is present' do - let(:gitaly_socket_path) { '/tmp/gitaly.sock' } + it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repo_path }) } + context 'when Gitaly is enabled' do before do - allow(Gitlab.config.gitaly).to receive(:socket_path).and_return(gitaly_socket_path) + allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) end it 'includes Gitaly params in the returned value' do - expect(subject).to include({ - GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs", - GitalySocketPath: gitaly_socket_path, + gitaly_socket_path = URI(Gitlab::GitalyClient.get_address('default')).path + expect(subject).to include({ GitalySocketPath: gitaly_socket_path }) + expect(subject[:Repository]).to include({ + path: repo_path, + storage_name: 'default', + relative_path: project.full_path + '.git', }) end end diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb deleted file mode 100644 index d968096783c..00000000000 --- a/spec/mailers/emails/builds_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' -require 'email_spec' - -describe Notify do - include EmailSpec::Matchers - - include_context 'gitlab email notification' - - describe 'build notification email' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - shared_examples 'build email' do - it 'contains name of project' do - is_expected.to have_body_text build.project_name - end - - it 'contains link to project' do - is_expected.to have_body_text namespace_project_path(project.namespace, project) - end - end - - shared_examples 'an email with X-GitLab headers containing build details' do - it 'has X-GitLab-Build* headers' do - is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ - is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ - end - end - - describe 'build success' do - subject { Notify.build_success_email(build.id, 'wow@example.com') } - before { build.success } - - it_behaves_like 'build email' - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'success' - end - - it 'has the correct subject' do - is_expected.to have_subject /Build success for/ - end - end - - describe 'build fail' do - subject { Notify.build_fail_email(build.id, 'wow@example.com') } - before { build.drop } - - it_behaves_like 'build email' - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'failed' - end - - it 'has the correct subject' do - is_expected.to have_subject /Build failed for/ - end - end - end -end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index e1877d5fde0..5ca936f28f0 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -5,6 +5,16 @@ describe Notify do include EmailSpec::Matchers include_context 'gitlab email notification' + shared_examples 'a new user email' do + it 'is sent to the new user with the correct subject and body' do + aggregate_failures do + is_expected.to deliver_to new_user_address + is_expected.to have_subject(/^Account was created for you$/i) + is_expected.to have_body_text(new_user_address) + end + end + end + describe 'profile notifications' do describe 'for new users, the email' do let(:example_site_path) { root_path } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 6ee91576676..f60c5ffb32a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -24,14 +24,14 @@ describe Notify do let(:previous_assignee) { create(:user, name: 'Previous Assignee') } shared_examples 'an assignee email' do - it 'is sent as the author' do - sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(current_user.name) - expect(sender.address).to eq(gitlab_sender) - end + it 'is sent to the assignee as the author' do + sender = subject.header[:from].addrs.first - it 'is sent to the assignee' do - is_expected.to deliver_to assignee.email + aggregate_failures do + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(assignee.email) + end end end @@ -49,12 +49,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(issue) - end - - it 'contains a link to the new issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end context 'when enabled email_author_in_body' do @@ -63,7 +62,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_html_escaped_body_text issue.author_name + is_expected.to have_html_escaped_body_text(issue.author_name) is_expected.to have_body_text 'wrote:' end end @@ -95,20 +94,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the name of the previous assignee' do - is_expected.to have_html_escaped_body_text previous_assignee.name - end - - it 'contains the name of the new assignee' do - is_expected.to have_html_escaped_body_text assignee.name - end - - it 'contains a link to the issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_html_escaped_body_text(previous_assignee.name) + is_expected.to have_html_escaped_body_text(assignee.name) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end @@ -129,16 +121,12 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the names of the added labels' do - is_expected.to have_body_text 'foo, bar, and baz' - end - - it 'contains a link to the issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text('foo, bar, and baz') + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end @@ -158,20 +146,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text status - end - - it 'contains the user name' do - is_expected.to have_html_escaped_body_text current_user.name - end - - it 'contains a link to the issue' do - is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(status) + is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) + end end end @@ -189,23 +170,21 @@ describe Notify do is_expected.to have_body_text 'Issue was moved to another project' end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains link to new issue' do + it 'has the correct subject and body' do new_issue_url = namespace_project_issue_path(new_issue.project.namespace, new_issue.project, new_issue) - is_expected.to have_body_text new_issue_url - end - it 'contains a link to the original issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(new_issue_url) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end end context 'for merge requests' do + let(:project) { create(:project, :repository) } let(:merge_author) { create(:user) } let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) } let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: FFaker::Lorem.sentence) } @@ -220,20 +199,13 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request) - end - - it 'contains a link to the new merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) - end - - it 'contains the source branch for the merge request' do - is_expected.to have_body_text merge_request.source_branch - end - - it 'contains the target branch for the merge request' do - is_expected.to have_body_text merge_request.target_branch + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + is_expected.to have_body_text(merge_request.source_branch) + is_expected.to have_body_text(merge_request.target_branch) + end end context 'when enabled email_author_in_body' do @@ -275,20 +247,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the name of the previous assignee' do - is_expected.to have_html_escaped_body_text previous_assignee.name - end - - it 'contains the name of the new assignee' do - is_expected.to have_html_escaped_body_text assignee.name - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_html_escaped_body_text(previous_assignee.name) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + is_expected.to have_html_escaped_body_text(assignee.name) + end end end @@ -309,16 +274,10 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do + it 'has the correct subject and body' do is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the names of the added labels' do - is_expected.to have_body_text 'foo, bar, and baz' - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + is_expected.to have_body_text('foo, bar, and baz') + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) end end @@ -338,20 +297,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text status - end - - it 'contains the user name' do - is_expected.to have_html_escaped_body_text current_user.name - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text(status) + is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + end end end @@ -371,23 +323,19 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text 'merged' - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text('merged') + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + end end end end end describe 'project was moved' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:user) { create(:user) } subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } @@ -395,16 +343,10 @@ describe Notify do it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" - it 'has the correct subject' do - is_expected.to have_subject "#{project.name} | Project was moved" - end - - it 'contains name of project' do + it 'has the correct subject and body' do + is_expected.to have_subject("#{project.name} | Project was moved") is_expected.to have_html_escaped_body_text project.name_with_namespace - end - - it 'contains new user role' do - is_expected.to have_body_text project.ssh_url_to_repo + is_expected.to have_body_text(project.ssh_url_to_repo) end end @@ -519,7 +461,7 @@ describe Notify do end describe 'project invitation' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) { invite_to_project(project, inviter: master) } @@ -539,7 +481,7 @@ describe Notify do end describe 'project invitation accepted' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:invited_user) { create(:user, name: 'invited user') } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) do @@ -564,7 +506,7 @@ describe Notify do end describe 'project invitation declined' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) do invitee = invite_to_project(project, inviter: master) @@ -597,14 +539,14 @@ describe Notify do shared_examples 'a note email' do it_behaves_like 'it should have Gmail Actions links' - it 'is sent as the author' do + it 'is sent to the given recipient as the author' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(note_author.name) - expect(sender.address).to eq(gitlab_sender) - end - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email + aggregate_failures do + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(recipient.notification_email) + end end it 'contains the message from the note' do @@ -628,6 +570,7 @@ describe Notify do end describe 'on a commit' do + let(:project) { create(:project, :repository) } let(:commit) { project.commit } before(:each) { allow(note).to receive(:noteable).and_return(commit) } @@ -641,12 +584,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'a user cannot unsubscribe through footer link' - it 'has the correct subject' do - is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})" - end - - it 'contains a link to the commit' do - is_expected.to have_body_text commit.short_id + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})") + is_expected.to have_body_text(commit.short_id) + end end end @@ -664,12 +606,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains a link to the merge request note' do - is_expected.to have_body_text note_on_merge_request_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text note_on_merge_request_path + end end end @@ -687,17 +628,17 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains a link to the issue note' do - is_expected.to have_body_text note_on_issue_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(note_on_issue_path) + end end end end context 'items that are noteable, emails for a note on a diff' do + let(:project) { create(:project, :repository) } let(:note_author) { create(:user, name: 'author_name') } before :each do @@ -717,14 +658,14 @@ describe Notify do it_behaves_like 'it should have Gmail Actions links' - it 'is sent as the author' do + it 'is sent to the given recipient as the author' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(note_author.name) - expect(sender.address).to eq(gitlab_sender) - end - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email + aggregate_failures do + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(recipient.notification_email) + end end it 'contains the message from the note' do @@ -934,21 +875,20 @@ describe Notify do is_expected.to deliver_to 'new-email@mail.com' end - it 'has the correct subject' do - is_expected.to have_subject 'Confirmation instructions | A Nice Suffix' - end - - it 'includes a link to the site' do - is_expected.to have_body_text example_site_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject('Confirmation instructions | A Nice Suffix') + is_expected.to have_body_text(example_site_path) + end end end describe 'email on push for a created branch' do let(:example_site_path) { root_path } let(:user) { create(:user) } - let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } + let(:tree_path) { namespace_project_tree_path(project.namespace, project, "empty-branch") } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -961,12 +901,11 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master" - end - - it 'contains a link to the branch' do - is_expected.to have_body_text tree_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}] Pushed new branch empty-branch") + is_expected.to have_body_text(tree_path) + end end end @@ -988,12 +927,11 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0" - end - - it 'contains a link to the tag' do - is_expected.to have_body_text tree_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}] Pushed new tag v1.0") + is_expected.to have_body_text(tree_path) + end end end @@ -1042,6 +980,7 @@ describe Notify do end describe 'email on push with multiple commits' do + let(:project) { create(:project, :repository) } let(:example_site_path) { root_path } let(:user) { create(:user) } let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } @@ -1064,24 +1003,14 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified" - end - - it 'includes commits list' do - is_expected.to have_body_text 'Change some files' - end - - it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' - end - - it 'contains a link to the diff' do - is_expected.to have_body_text diff_path - end - - it 'does not contain the misleading footer' do - is_expected.not_to have_body_text 'you are a member of' + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified") + is_expected.to have_body_text('Change some files') + is_expected.to have_body_text('def</span> <span class="nf">archive_formats_regex') + is_expected.to have_body_text(diff_path) + is_expected.not_to have_body_text('you are a member of') + end end context "when set to send from committer email if domain matches" do @@ -1098,13 +1027,13 @@ describe Notify do end it "is sent from the committer email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(user.email) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the committer email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(user.email) + aggregate_failures do + expect(from.address).to eq(user.email) + expect(reply.address).to eq(user.email) + end end end @@ -1115,13 +1044,13 @@ describe Notify do end it "is sent from the default email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(gitlab_sender) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the default email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(gitlab_sender_reply_to) + aggregate_failures do + expect(from.address).to eq(gitlab_sender) + expect(reply.address).to eq(gitlab_sender_reply_to) + end end end @@ -1132,19 +1061,20 @@ describe Notify do end it "is sent from the default email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(gitlab_sender) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the default email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(gitlab_sender_reply_to) + aggregate_failures do + expect(from.address).to eq(gitlab_sender) + expect(reply.address).to eq(gitlab_sender_reply_to) + end end end end end describe 'email on push with a single commit' do + let(:project) { create(:project, :repository) } let(:example_site_path) { root_path } let(:user) { create(:user) } let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } @@ -1166,25 +1096,18 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}" - end - - it 'includes commits list' do - is_expected.to have_body_text 'Change some files' - end - - it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' - end - - it 'contains a link to the diff' do - is_expected.to have_body_text diff_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}][master] #{commits.first.title}") + is_expected.to have_body_text('Change some files') + is_expected.to have_body_text('def</span> <span class="nf">archive_formats_regex') + is_expected.to have_body_text(diff_path) + end end end describe 'HTML emails setting' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb new file mode 100644 index 00000000000..57eb03e3c80 --- /dev/null +++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb') + +# This migration uses multiple threads, and thus different transactions. This +# means data created in this spec may not be visible to some threads. To work +# around this we use the TRUNCATE cleaning strategy. +describe MigrateBuildEventsToPipelineEvents, truncate: true do + let(:migration) { described_class.new } + let(:project_with_pipeline_service) { create(:empty_project) } + let(:project_with_build_service) { create(:empty_project) } + + before do + ActiveRecord::Base.connection.execute <<-SQL + INSERT INTO services (properties, build_events, pipeline_events, type) + VALUES + ('{"notify_only_broken_builds":true}', true, false, 'SlackService') + , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService') + , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService') + ; + SQL + + ActiveRecord::Base.connection.execute <<-SQL + INSERT INTO services + (properties, build_events, pipeline_events, type, project_id) + VALUES + ('{"notify_only_broken_builds":true}', true, false, + 'BuildsEmailService', #{project_with_pipeline_service.id}) + , ('{"notify_only_broken_pipelines":true}', false, true, + 'PipelinesEmailService', #{project_with_pipeline_service.id}) + , ('{"notify_only_broken_builds":true}', true, false, + 'BuildsEmailService', #{project_with_build_service.id}) + ; + SQL + end + + describe '#up' do + before do + silence_migration = Module.new do + # rubocop:disable Rails/Delegate + def execute(query) + connection.execute(query) + end + end + + migration.extend(silence_migration) + migration.up + end + + it 'migrates chat service properly' do + [SlackService, MattermostService, HipchatService].each do |service| + expect(service.count).to eq(1) + + verify_service_record(service.first) + end + end + + it 'migrates pipelines email service only if it has none before' do + Project.find_each do |project| + pipeline_service_count = + project.services.where(type: 'PipelinesEmailService').count + + expect(pipeline_service_count).to eq(1) + + verify_service_record(project.pipelines_email_service) + end + end + + def verify_service_record(service) + expect(service.notify_only_broken_pipelines).to be(true) + expect(service.build_events).to be(false) + expect(service.pipeline_events).to be(true) + end + end +end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index b6d678bac18..3db57595fa6 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') describe MigrateProcessCommitWorkerJobs do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:commit) { project.commit.raw.raw_commit } diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 94c25a454aa..552229e9b07 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -53,6 +53,20 @@ describe Blob do end end + describe '#ipython_notebook?' do + it 'is falsey when language is not Jupyter Notebook' do + git_blob = double(text?: true, language: double(name: 'JSON')) + + expect(described_class.decorate(git_blob)).not_to be_ipython_notebook + end + + it 'is truthy when language is Jupyter Notebook' do + git_blob = double(text?: true, language: double(name: 'Jupyter Notebook')) + + expect(described_class.decorate(git_blob)).to be_ipython_notebook + end + end + describe '#video?' do it 'is falsey with image extension' do git_blob = Gitlab::Git::Blob.new(name: 'image.png') @@ -116,6 +130,11 @@ describe Blob do blob = stubbed_blob expect(blob.to_partial_path(project)).to eq 'download' end + + it 'handles iPython notebooks' do + blob = stubbed_blob(text?: true, ipython_notebook?: true) + expect(blob.to_partial_path(project)).to eq 'notebook' + end end describe '#size_within_svg_limits?' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index fd6ea2d6722..8dbcf50ee0c 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -795,8 +795,8 @@ describe Ci::Build, :models do describe '#merge_request' do def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: pipeline.gl_project_id, - target_project_id: pipeline.gl_project_id, + create(factory, source_project: pipeline.project, + target_project: pipeline.project, source_branch: build.ref, created_at: created_at) end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index bee9f714849..048d25869bc 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -6,7 +6,7 @@ describe Ci::Variable, models: true do let(:secret_value) { 'secret' } it { is_expected.to validate_presence_of(:key) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } it { is_expected.to validate_length_of(:key).is_at_most(255) } it { is_expected.to allow_value('foo').for(:key) } it { is_expected.not_to allow_value('foo bar').for(:key) } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 4b449546a30..980a1b70ef5 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -388,4 +388,32 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end + + describe '#raw_diffs' do + context 'Gitaly commit_raw_diffs feature enabled' do + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true) + end + + context 'when a truthy deltas_only is not passed to args' do + it 'fetches diffs from Gitaly server' do + expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent). + with(commit) + + commit.raw_diffs + end + end + + context 'when a truthy deltas_only is passed to args' do + it 'fetches diffs using Rugged' do + opts = { deltas_only: true } + + expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent) + expect(commit.raw).to receive(:diffs).with(opts) + + commit.raw_diffs(opts) + end + end + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index ea5e4e21039..7343b735a74 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -297,4 +297,40 @@ describe CommitStatus, :models do end end end + + describe '#locking_enabled?' do + before do + commit_status.lock_version = 100 + end + + subject { commit_status.locking_enabled? } + + context "when changing status" do + before do + commit_status.status = "running" + end + + it "lock" do + is_expected.to be true + end + + it "raise exception when trying to update" do + expect{ commit_status.save }.to raise_error(ActiveRecord::StaleObjectError) + end + end + + context "when changing description" do + before do + commit_status.description = "test" + end + + it "do not lock" do + is_expected.to be false + end + + it "save correctly" do + expect(commit_status.save).to be true + end + end + end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index f134da441c2..82abad0e2f6 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -110,6 +110,24 @@ describe HasStatus do it { is_expected.to eq 'running' } end + context 'when one status finished and second is still created' do + let!(:statuses) do + [create(type, status: :success), create(type, status: :created)] + end + + it { is_expected.to eq 'running' } + end + + context 'when there is a manual status before created status' do + let!(:statuses) do + [create(type, status: :success), + create(type, status: :manual, allow_failure: false), + create(type, status: :created)] + end + + it { is_expected.to eq 'manual' } + end + context 'when one status is a blocking manual action' do let!(:statuses) do [create(type, status: :failed), diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 9574796a945..4522206fab1 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -44,6 +44,34 @@ describe Issue, "Issuable" do it { expect(described_class).to respond_to(:assigned) } end + describe 'author_name' do + it 'is delegated to author' do + expect(issue.author_name).to eq issue.author.name + end + + it 'returns nil when author is nil' do + issue.author_id = nil + issue.save(validate: false) + + expect(issue.author_name).to eq nil + end + end + + describe 'assignee_name' do + it 'is delegated to assignee' do + issue.update!(assignee: create(:user)) + + expect(issue.assignee_name).to eq issue.assignee.name + end + + it 'returns nil when assignee is nil' do + issue.assignee_id = nil + issue.save(validate: false) + + expect(issue.assignee_name).to eq nil + end + end + describe "before_save" do describe "#update_cache_counts" do context "when previous assignee exists" do diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index e8caad00c44..8acec805584 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -6,6 +6,9 @@ describe SystemHook, models: true do let(:user) { create(:user) } let(:project) { create(:empty_project, namespace: user.namespace) } let(:group) { create(:group) } + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' } + end before do WebMock.stub_request(:post, system_hook.url) @@ -29,7 +32,7 @@ describe SystemHook, models: true do end it "user_create hook" do - create(:user) + Users::CreateService.new(nil, params).execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 9ffcb88bafd..b8584301baa 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -37,6 +37,30 @@ describe Issue, models: true do end end + describe '#closed_at' do + after do + Timecop.return + end + + let!(:now) { Timecop.freeze(Time.now) } + + it 'sets closed_at to Time.now when issue is closed' do + issue = create(:issue, state: 'opened') + + issue.close + + expect(issue.closed_at).to eq(now) + end + + it 'sets closed_at to nil when issue is reopened' do + issue = create(:issue, state: 'closed') + + issue.reopen + + expect(issue.closed_at).to be_nil + end + end + describe '#to_reference' do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } @@ -646,4 +670,41 @@ describe Issue, models: true do expect(attrs_hash).to include('time_estimate') end end + + describe '#check_for_spam' do + let(:project) { create :project, visibility_level: visibility_level } + let(:issue) { create :issue, project: project } + + subject do + issue.assign_attributes(description: description) + issue.check_for_spam? + end + + context 'when project is public and spammable attributes changed' do + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:description) { 'woo' } + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when project is private' do + let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } + let(:description) { issue.description } + + it 'returns false' do + is_expected.to be_falsey + end + end + + context 'when spammable attributes have not changed' do + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + let(:description) { issue.description } + + it 'returns false' do + is_expected.to be_falsey + end + end + end end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index e6ca4853873..db2c2619968 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -19,8 +19,8 @@ describe List do expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id) end - context 'when list_type is set to done' do - subject { described_class.new(list_type: :done) } + context 'when list_type is set to closed' do + subject { described_class.new(list_type: :closed) } it { is_expected.not_to validate_presence_of(:label) } it { is_expected.not_to validate_presence_of(:position) } @@ -34,8 +34,8 @@ describe List do expect(subject.destroy).to be_truthy end - it 'can not be destroyed when when list_type is set to done' do - subject = create(:done_list) + it 'can not be destroyed when when list_type is set to closed' do + subject = create(:closed_list) expect(subject.destroy).to be_falsey end @@ -48,8 +48,8 @@ describe List do expect(subject).to be_destroyable end - it 'returns false when list_type is set to done' do - subject.list_type = :done + it 'returns false when list_type is set to closed' do + subject.list_type = :closed expect(subject).not_to be_destroyable end @@ -62,8 +62,8 @@ describe List do expect(subject).to be_movable end - it 'returns false when list_type is set to done' do - subject.list_type = :done + it 'returns false when list_type is set to closed' do + subject.list_type = :closed expect(subject).not_to be_movable end @@ -77,10 +77,10 @@ describe List do expect(subject.title).to eq 'Development' end - it 'returns Done when list_type is set to done' do - subject.list_type = :done + it 'returns Closed when list_type is set to closed' do + subject.list_type = :closed - expect(subject.title).to eq 'Done' + expect(subject.title).to eq 'Closed' end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 3cee2b7714f..f3f48f951a8 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -109,7 +109,7 @@ describe Milestone, models: true do it { expect(milestone.percent_complete(user)).to eq(75) } end - describe :items_count do + describe '#is_empty?' do before do milestone.issues << create(:issue, project: project) milestone.issues << create(:closed_issue, project: project) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 757f3921450..ccaf0d7abc7 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -129,10 +129,10 @@ describe Namespace, models: true do end end - describe '#move_dir' do + describe '#move_dir', repository: true do before do @namespace = create :namespace - @project = create(:empty_project, namespace: @namespace) + @project = create(:project_empty_repo, namespace: @namespace) allow(@namespace).to receive(:path_changed?).and_return(true) end @@ -141,9 +141,9 @@ describe Namespace, models: true do end it "moves dir if path changed" do - new_path = @namespace.path + "_new" - allow(@namespace).to receive(:path_was).and_return(@namespace.path) - allow(@namespace).to receive(:path).and_return(new_path) + new_path = @namespace.full_path + "_new" + allow(@namespace).to receive(:full_path_was).and_return(@namespace.full_path) + allow(@namespace).to receive(:full_path).and_return(new_path) expect(@namespace).to receive(:remove_exports!) expect(@namespace.move_dir).to be_truthy end @@ -161,16 +161,75 @@ describe Namespace, models: true do it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') } end + + context 'renaming a sub-group' do + let(:parent) { create(:group, name: 'parent', path: 'parent') } + let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } + let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) } + let(:uploads_dir) { File.join(CarrierWave.root, 'uploads', 'parent') } + let(:pages_dir) { File.join(TestEnv.pages_path, 'parent') } + + before do + FileUtils.mkdir_p(File.join(uploads_dir, 'child', 'the-project')) + FileUtils.mkdir_p(File.join(pages_dir, 'child', 'the-project')) + end + + it 'correctly moves the repository, uploads and pages' do + expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git') + expected_upload_path = File.join(uploads_dir, 'renamed', 'the-project') + expected_pages_path = File.join(pages_dir, 'renamed', 'the-project') + + child.update_attributes!(path: 'renamed') + + expect(File.directory?(expected_repository_path)).to be(true) + expect(File.directory?(expected_upload_path)).to be(true) + expect(File.directory?(expected_pages_path)).to be(true) + end + end end - describe :rm_dir do - let!(:project) { create(:empty_project, namespace: namespace) } - let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) } + describe '#rm_dir', 'callback', repository: true do + let!(:project) { create(:project_empty_repo, namespace: namespace) } + let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] } + let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) } + let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") } + let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } + + it 'renames its dirs when deleted' do + allow(GitlabShellWorker).to receive(:perform_in) - it "removes its dirs when deleted" do namespace.destroy - expect(File.exist?(path)).to be(false) + expect(File.exist?(deleted_path_in_dir)).to be(true) + end + + it 'schedules the namespace for deletion' do + expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path) + + namespace.destroy + end + + context 'in sub-groups' do + let(:parent) { create(:namespace, path: 'parent') } + let(:child) { create(:namespace, parent: parent, path: 'child') } + let!(:project) { create(:project_empty_repo, namespace: child) } + let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') } + let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") } + let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } + + it 'renames its dirs when deleted' do + allow(GitlabShellWorker).to receive(:perform_in) + + child.destroy + + expect(File.exist?(deleted_path_in_dir)).to be(true) + end + + it 'schedules the namespace for deletion' do + expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path) + + child.destroy + end end it 'removes the exports folder' do @@ -215,10 +274,12 @@ describe Namespace, models: true do end describe '#descendants' do - let!(:group) { create(:group) } + let!(:group) { create(:group, path: 'git_lab') } let!(:nested_group) { create(:group, parent: group) } let!(:deep_nested_group) { create(:group, parent: nested_group) } let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } it 'returns the correct descendants' do expect(very_deep_nested_group.descendants.to_a).to eq([]) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index e6a4583a8fb..c6c45d78990 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -5,7 +5,7 @@ describe PagesDomain, models: true do it { is_expected.to belong_to(:project) } end - describe :validate_domain do + describe 'validate domain' do subject { build(:pages_domain, domain: domain) } context 'is unique' do @@ -75,7 +75,7 @@ describe PagesDomain, models: true do end end - describe :url do + describe '#url' do subject { domain.url } context 'without the certificate' do @@ -91,7 +91,7 @@ describe PagesDomain, models: true do end end - describe :has_matching_key? do + describe '#has_matching_key?' do subject { domain.has_matching_key? } context 'for matching key' do @@ -107,7 +107,7 @@ describe PagesDomain, models: true do end end - describe :has_intermediates? do + describe '#has_intermediates?' do subject { domain.has_intermediates? } context 'for self signed' do @@ -133,7 +133,7 @@ describe PagesDomain, models: true do end end - describe :expired? do + describe '#expired?' do subject { domain.expired? } context 'for valid' do @@ -149,7 +149,7 @@ describe PagesDomain, models: true do end end - describe :subject do + describe '#subject' do let(:domain) { build(:pages_domain, :with_certificate) } subject { domain.subject } @@ -157,7 +157,7 @@ describe PagesDomain, models: true do it { is_expected.to eq('/CN=test-certificate') } end - describe :certificate_text do + describe '#certificate_text' do let(:domain) { build(:pages_domain, :with_certificate) } subject { domain.certificate_text } diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb deleted file mode 100644 index 0194f9e2563..00000000000 --- a/spec/models/project_services/builds_email_service_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' - -describe BuildsEmailService do - let(:data) do - Gitlab::DataBuilder::Build.build(create(:ci_build)) - end - - describe 'Validations' do - context 'when service is active' do - before { subject.active = true } - - it { is_expected.to validate_presence_of(:recipients) } - - context 'when pusher is added' do - before { subject.add_pusher = true } - - it { is_expected.not_to validate_presence_of(:recipients) } - end - end - - context 'when service is inactive' do - before { subject.active = false } - - it { is_expected.not_to validate_presence_of(:recipients) } - end - end - - describe '#test_data' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - let(:user) { create(:user) } - - before { project.team << [user, :developer] } - - it 'builds test data' do - data = subject.test_data(project) - - expect(data[:object_kind]).to eq("build") - end - end - - describe '#test' do - it 'sends email' do - data = Gitlab::DataBuilder::Build.build(create(:ci_build)) - subject.recipients = 'test@gitlab.com' - - expect(BuildEmailWorker).to receive(:perform_async) - - subject.test(data) - end - - context 'notify only failed builds is true' do - it 'sends email' do - data = Gitlab::DataBuilder::Build.build(create(:ci_build)) - data[:build_status] = "success" - subject.recipients = 'test@gitlab.com' - - expect(subject).not_to receive(:notify_only_broken_builds) - expect(BuildEmailWorker).to receive(:perform_async) - - subject.test(data) - end - end - end - - describe '#execute' do - it 'sends email' do - subject.recipients = 'test@gitlab.com' - data[:build_status] = 'failed' - - expect(BuildEmailWorker).to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with succeeded build and notify_only_broken_builds on' do - expect(subject).to receive(:notify_only_broken_builds).and_return(true) - data[:build_status] = 'success' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with failed build and build_allow_failure is true' do - data[:build_status] = 'failed' - data[:build_allow_failure] = true - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email with unknown build status' do - data[:build_status] = 'foo' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - - it 'does not send email when recipients list is empty' do - subject.recipients = ' ,, ' - data[:build_status] = 'failed' - - expect(BuildEmailWorker).not_to receive(:perform_async) - - subject.execute(data) - end - end -end diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb deleted file mode 100644 index 3bd7ec18ae0..00000000000 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe ChatMessage::BuildMessage do - subject { described_class.new(args) } - - let(:args) do - { - sha: '97de212e80737a608d939f648d959671fb0a0142', - ref: 'develop', - tag: false, - - project_name: 'project_name', - project_url: 'http://example.gitlab.com', - build_id: 1, - build_name: build_name, - build_stage: stage, - - commit: { - status: status, - author_name: 'hacker', - author_url: 'http://example.gitlab.com/hacker', - duration: duration, - }, - } - end - - let(:message) { build_message } - let(:stage) { 'test' } - let(:status) { 'success' } - let(:build_name) { 'rspec' } - let(:duration) { 10 } - - context 'build succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - context 'build failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - - it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - it 'returns a message with information on build' do - expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>") - end - - it 'returns a message with stage name' do - expect(subject.fallback).to include("of stage #{stage}") - end - - it 'returns a message with link to author' do - expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>") - end - - def build_message(status_text = status, stage_text = stage, build_text = build_name) - "<http://example.gitlab.com|project_name>:" \ - " Commit <http://example.gitlab.com/commit/" \ - "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ - " of <http://example.gitlab.com/commits/develop|develop> branch" \ - " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \ - " on build <http://example.gitlab.com/builds/1|#{build_text}>" \ - " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index bf422ac7ce1..1200ae7eb22 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -280,13 +280,14 @@ describe HipchatService, models: true do end end - context 'build events' do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:data) { Gitlab::DataBuilder::Build.build(build.reload) } + context 'pipeline events' do + let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) } + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'for failed' do - before { build.drop } + before do + pipeline.drop + end it "calls Hipchat API" do hipchat.execute(data) @@ -295,35 +296,36 @@ describe HipchatService, models: true do end it "creates a build message" do - message = hipchat.send(:create_build_message, data) + message = hipchat.__send__(:create_pipeline_message, data) project_url = project.web_url project_name = project.name_with_namespace.gsub(/\s/, '') - sha = data[:sha] - ref = data[:ref] - ref_type = data[:tag] ? 'tag' : 'branch' - duration = data[:commit][:duration] + pipeline_attributes = data[:object_attributes] + ref = pipeline_attributes[:ref] + ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + duration = pipeline_attributes[:duration] + user_name = data[:user][:name] expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \ - "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \ + "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \ "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \ - "by #{data[:commit][:author_name]} failed in #{duration} second(s)") + "by #{user_name} failed in #{duration} second(s)") end end context 'for succeeded' do before do - build.success + pipeline.succeed end it "calls Hipchat API" do - hipchat.notify_only_broken_builds = false + hipchat.notify_only_broken_pipelines = false hipchat.execute(data) expect(WebMock).to have_requested(:post, api_url).once end it "notifies only broken" do - hipchat.notify_only_broken_builds = true + hipchat.notify_only_broken_pipelines = true hipchat.execute(data) expect(WebMock).not_to have_requested(:post, api_url).once end @@ -349,17 +351,19 @@ describe HipchatService, models: true do context 'with a successful build' do it 'uses the green color' do - build_data = { object_kind: 'build', commit: { status: 'success' } } + data = { object_kind: 'pipeline', + object_attributes: { status: 'success' } } - expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' }) + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' }) end end context 'with a failed build' do it 'uses the red color' do - build_data = { object_kind: 'build', commit: { status: 'failed' } } + data = { object_kind: 'pipeline', + object_attributes: { status: 'failed' } } - expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' }) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ff1defcd32d..59a2560ca06 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,8 +29,7 @@ describe Project, models: true do it { is_expected.to have_one(:campfire_service).dependent(:destroy) } it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) } it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } - it { is_expected.to have_one(:builds_email_service).dependent(:destroy) } - it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } + it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) } it { is_expected.to have_one(:irker_service).dependent(:destroy) } it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) } it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } @@ -219,6 +218,20 @@ describe Project, models: true do expect(project2.import_data).to be_nil end + it "does not allow blocked import_url localhost" do + project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git') + + expect(project2).to be_invalid + expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + end + + it "does not allow blocked import_url port" do + project2 = build(:empty_project, import_url: 'http://github.com:25/t.git') + + expect(project2).to be_invalid + expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + end + describe 'project pending deletion' do let!(:project_pending_deletion) do create(:empty_project, @@ -1749,11 +1762,14 @@ describe Project, models: true do end describe 'inside_path' do - let!(:project1) { create(:empty_project) } + let!(:project1) { create(:empty_project, namespace: create(:namespace, path: 'name_pace')) } let!(:project2) { create(:empty_project) } + let!(:project3) { create(:empty_project, namespace: create(:namespace, path: 'namespace')) } let!(:path) { project1.namespace.full_path } - it { expect(Project.inside_path(path)).to eq([project1]) } + it 'returns correct project' do + expect(Project.inside_path(path)).to eq([project1]) + end end describe '#route_map_for' do @@ -1901,10 +1917,8 @@ describe Project, models: true do context 'when no user is given' do it 'returns the url to the repo without a username' do - url = project.http_url_to_repo - - expect(url).to eq(project.http_url_to_repo) - expect(url).not_to include('@') + expect(project.http_url_to_repo).to eq("#{project.web_url}.git") + expect(project.http_url_to_repo).not_to include('@') end end @@ -1912,7 +1926,7 @@ describe Project, models: true do it 'returns the url to the repo with the username' do user = build_stubbed(:user) - expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@}) + expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@") end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 58b57bd4fef..b5b9cd024b0 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -35,10 +35,23 @@ describe ProjectWiki, models: true do end describe "#http_url_to_repo" do - it "provides the full http url to the repo" do - gitlab_url = Gitlab.config.gitlab.url - repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git" - expect(subject.http_url_to_repo).to eq(repo_http_url) + let(:project) { create :empty_project } + + context 'when no user is given' do + it 'returns the url to the repo without a username' do + expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git" + + expect(project_wiki.http_url_to_repo).to eq(expected_url) + expect(project_wiki.http_url_to_repo).not_to include('@') + end + end + + context 'when user is given' do + it 'returns the url to the repo with the username' do + user = build_stubbed(:user) + + expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@") + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 274e4f00a0a..df742ee8084 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1083,7 +1083,7 @@ describe Repository, models: true do end end - describe :skip_merged_commit do + describe 'skip_merges option' do subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } } it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } @@ -1851,4 +1851,17 @@ describe Repository, models: true do end end end + + describe '#is_ancestor?' do + context 'Gitaly is_ancestor feature enabled' do + it 'asks Gitaly server if it\'s an ancestor' do + commit = repository.commit + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor). + with(repository.raw_repository, commit.id, commit.id).and_return(true) + + expect(repository.is_ancestor?(commit.id, commit.id)).to be true + end + end + end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 0b222022e62..171a51fcc5b 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Route, models: true do - let!(:group) { create(:group, path: 'gitlab', name: 'gitlab') } + let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') } let!(:route) { group.route } describe 'relationships' do @@ -14,10 +14,24 @@ describe Route, models: true do it { is_expected.to validate_uniqueness_of(:path) } end + describe '.inside_path' do + let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) } + let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) } + let!(:another_group) { create(:group, path: 'other') } + let!(:similar_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'another', name: 'another', parent: similar_group) } + + it 'returns correct routes' do + expect(Route.inside_path('git_lab')).to match_array([nested_group.route, deep_nested_group.route]) + end + end + describe '#rename_descendants' do let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) } let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) } let!(:similar_group) { create(:group, path: 'gitlab-org', name: 'gitlab-org') } + let!(:another_group) { create(:group, path: 'gittlab', name: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'git_lab', name: 'git_lab', parent: another_group) } context 'path update' do context 'when route name is set' do @@ -28,6 +42,8 @@ describe Route, models: true do expect(described_class.exists?(path: 'bar/test')).to be_truthy expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy expect(described_class.exists?(path: 'gitlab-org')).to be_truthy + expect(described_class.exists?(path: 'gittlab')).to be_truthy + expect(described_class.exists?(path: 'gittlab/git_lab')).to be_truthy end end @@ -43,14 +59,22 @@ describe Route, models: true do end context 'name update' do - before { route.update_attributes(name: 'bar') } - it "updates children routes with new path" do + route.update_attributes(name: 'bar') + expect(described_class.exists?(name: 'bar')).to be_truthy expect(described_class.exists?(name: 'bar / test')).to be_truthy expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy expect(described_class.exists?(name: 'gitlab-org')).to be_truthy end + + it 'handles a rename from nil' do + # Note: using `update_columns` to skip all validation and callbacks + route.update_columns(name: nil) + + expect { route.update_attributes(name: 'bar') } + .to change { route.name }.from(nil).to('bar') + end end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 219ab1989ea..8095d01b69e 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -198,4 +198,47 @@ describe Snippet, models: true do expect(snippet.participants).to include(note1.author, note2.author) end end + + describe '#check_for_spam' do + let(:snippet) { create :snippet, visibility_level: visibility_level } + + subject do + snippet.assign_attributes(title: title) + snippet.check_for_spam? + end + + context 'when public and spammable attributes changed' do + let(:visibility_level) { Snippet::PUBLIC } + let(:title) { 'woo' } + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when private' do + let(:visibility_level) { Snippet::PRIVATE } + let(:title) { snippet.title } + + it 'returns false' do + is_expected.to be_falsey + end + + it 'returns true when switching to public' do + snippet.save! + snippet.visibility_level = Snippet::PUBLIC + + expect(snippet.check_for_spam?).to be_truthy + end + end + + context 'when spammable attributes have not changed' do + let(:visibility_level) { Snippet::PUBLIC } + let(:title) { snippet.title } + + it 'returns false' do + is_expected.to be_falsey + end + end + end end diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb new file mode 100644 index 00000000000..d238e28209a --- /dev/null +++ b/spec/models/system_note_metadata_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe SystemNoteMetadata, models: true do + describe 'associations' do + it { is_expected.to belong_to(:note) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:note) } + + context 'when action type is invalid' do + subject do + build(:system_note_metadata, note: build(:note), action: 'invalid_type' ) + end + + it { is_expected.to be_invalid } + end + + context 'when action type is valid' do + subject do + build(:system_note_metadata, note: build(:note), action: 'merge' ) + end + + it { is_expected.to be_valid } + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 90378179e32..a9e37be1157 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -81,6 +81,7 @@ describe User, models: true do it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } + it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) } it { is_expected.to validate_length_of(:bio).is_at_most(255) } @@ -360,22 +361,10 @@ describe User, models: true do end describe '#generate_password' do - it "executes callback when force_random_password specified" do - user = build(:user, force_random_password: true) - expect(user).to receive(:generate_password) - user.save - end - it "does not generate password by default" do user = create(:user, password: 'abcdefghe') expect(user.password).to eq('abcdefghe') end - - it "generates password when forcing random password" do - allow(Devise).to receive(:friendly_token).and_return('123456789') - user = create(:user, password: 'abcdefg', force_random_password: true) - expect(user.password).to eq('12345678') - end end describe 'authentication token' do diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 7591bfd1471..2905d5b26a5 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -5,7 +5,7 @@ describe IssuePolicy, models: true do describe '#rules' do context 'using a regular issue' do - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } let(:policies) { described_class.abilities(user, issue).to_set } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 0a5edf35f59..064847ee3dc 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -74,7 +74,7 @@ describe ProjectPolicy, models: true do end it 'does not include the read_issue permission when the issue author is not a member of the private project' do - project = create(:project, :private) + project = create(:empty_project, :private) issue = create(:issue, project: project) user = issue.author diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index ab5a7e4d3de..a70f7beaae0 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -5,77 +5,146 @@ describe API::Branches, api: true do include ApiHelpers let(:user) { create(:user) } - let(:user2) { create(:user) } let!(:project) { create(:project, :repository, creator: user) } let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } + let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] } describe "GET /projects/:id/repository/branches" do - it "returns an array of project branches" do - project.repository.expire_all_method_caches + let(:route) { "/projects/#{project.id}/repository/branches" } - get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + shared_examples_for 'repository branches' do + it 'returns the repository branches' do + get api(route, current_user), per_page: 100 - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - end - describe "GET /projects/:id/repository/branches/:branch" do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branches' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end - expect(json_response['name']).to eq(branch_name) - json_commit = json_response['commit'] - expect(json_commit['id']).to eq(branch_sha) - expect(json_commit).to have_key('short_id') - expect(json_commit).to have_key('title') - expect(json_commit).to have_key('message') - expect(json_commit).to have_key('author_name') - expect(json_commit).to have_key('author_email') - expect(json_commit).to have_key('authored_date') - expect(json_commit).to have_key('committer_name') - expect(json_commit).to have_key('committer_email') - expect(json_commit).to have_key('committed_date') - expect(json_commit).to have_key('parent_ids') - expect(json_response['merged']).to eq(false) - expect(json_response['protected']).to eq(false) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "returns the branch information for a single branch with dots in the name" do - get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository branches' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end + end + + describe "GET /projects/:id/repository/branches/:branch" do + let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" } - context 'on a merged branch' do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/merge-test", user) + shared_examples_for 'repository branch' do |merged: false| + it 'returns the repository branch' do + get api(route, current_user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq('merge-test') - expect(json_response['merged']).to eq(true) + expect(json_response['name']).to eq(branch_name) + expect(json_response['merged']).to eq(merged) + expect(json_response['protected']).to eq(false) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + + json_commit = json_response['commit'] + expect(json_commit['id']).to eq(branch_sha) + expect(json_commit).to have_key('short_id') + expect(json_commit).to have_key('title') + expect(json_commit).to have_key('message') + expect(json_commit).to have_key('author_name') + expect(json_commit).to have_key('author_email') + expect(json_commit).to have_key('authored_date') + expect(json_commit).to have_key('committer_name') + expect(json_commit).to have_key('committer_email') + expect(json_commit).to have_key('committed_date') + expect(json_commit).to have_key('parent_ids') + end + + context 'when branch does not exist' do + let(:branch_name) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Branch Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end end end - it "returns a 403 error if guest" do - get api("/projects/#{project.id}/repository/branches", user2) - expect(response).to have_http_status(403) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branch' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - it "returns a 404 error if branch is not available" do - get api("/projects/#{project.id}/repository/branches/unknown", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + let(:current_user) { user } + it_behaves_like 'repository branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + let(:branch_sha) { project.commit('master').sha } + + it_behaves_like 'repository branch' + end + + context 'when branch is merged' do + let(:branch_name) { 'merge-test' } + let(:branch_sha) { project.commit('merge-test').sha } + + it_behaves_like 'repository branch', merged: true + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end @@ -93,10 +162,10 @@ describe API::Branches, api: true do end it "protects a single branch with dots in the name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(true) end @@ -234,7 +303,7 @@ describe API::Branches, api: true do end it "returns a 403 error if guest" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest) expect(response).to have_http_status(403) end end @@ -250,10 +319,10 @@ describe API::Branches, api: true do end it "update branches with dots in branch name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(false) end @@ -282,7 +351,7 @@ describe API::Branches, api: true do end it "denies for user without push access" do - post api("/projects/#{project.id}/repository/branches", user2), + post api("/projects/#{project.id}/repository/branches", guest), branch: branch_name, ref: branch_sha expect(response).to have_http_status(403) @@ -330,7 +399,7 @@ describe API::Branches, api: true do end it "removes a branch with dots in the branch name" do - delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) expect(response).to have_http_status(204) end @@ -367,7 +436,7 @@ describe API::Branches, api: true do end it 'returns a 403 error if guest' do - delete api("/projects/#{project.id}/repository/merged_branches", user2) + delete api("/projects/#{project.id}/repository/merged_branches", guest) expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 63ec00cdf04..eed45d37444 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -424,12 +424,12 @@ describe API::Internal, api: true do end before do - allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) end it "calls the Gitaly client if it's enabled" do expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive).with(project.repository.path) + to receive(:post_receive) post api("/internal/notify_post_receive"), valid_params @@ -438,7 +438,7 @@ describe API::Internal, api: true do it "returns 500 if the gitaly call fails" do expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable) + to receive(:post_receive).and_raise(GRPC::Unavailable) post api("/internal/notify_post_receive"), valid_params diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index e7738ca3034..91d6fb83c0b 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -333,8 +333,16 @@ describe API::Issues, api: true do end let(:base_url) { "/groups/#{group.id}/issues" } + it 'returns all group issues (including opened and closed)' do + get api(base_url, admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + end + it 'returns group issues without confidential issues for non project members' do - get api(base_url, non_member) + get api("#{base_url}?state=opened", non_member) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -344,7 +352,7 @@ describe API::Issues, api: true do end it 'returns group confidential issues for author' do - get api(base_url, author) + get api("#{base_url}?state=opened", author) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -353,7 +361,7 @@ describe API::Issues, api: true do end it 'returns group confidential issues for assignee' do - get api(base_url, assignee) + get api("#{base_url}?state=opened", assignee) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -362,7 +370,7 @@ describe API::Issues, api: true do end it 'returns group issues with confidential issues for project members' do - get api(base_url, user) + get api("#{base_url}?state=opened", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -371,7 +379,7 @@ describe API::Issues, api: true do end it 'returns group confidential issues for admin' do - get api(base_url, admin) + get api("#{base_url}?state=opened", admin) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -460,7 +468,7 @@ describe API::Issues, api: true do end it 'returns an array of issues in given milestone' do - get api("#{base_url}?milestone=#{group_milestone.title}", user) + get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -534,6 +542,12 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9aba1d75612..61d965e8974 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -623,64 +623,6 @@ describe API::MergeRequests, api: true do end end - describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do - it "returns comment" do - original_count = merge_request.notes.size - - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment" - - expect(response).to have_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['author']['name']).to eq(user.name) - expect(json_response['author']['username']).to eq(user.username) - expect(merge_request.reload.notes.size).to eq(original_count + 1) - end - - it "returns 400 if note is missing" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) - expect(response).to have_http_status(400) - end - - it "returns 404 if merge request iid is invalid" do - post api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' - expect(response).to have_http_status(404) - end - - it "returns 404 if merge request id is used instead of iid" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), - note: 'My comment' - expect(response).to have_http_status(404) - end - end - - describe "GET :id/merge_requests/:merge_request_iid/comments" do - let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } - let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } - - it "returns merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) - - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq("a comment on a MR") - expect(json_response.first['author']['id']).to eq(user.id) - expect(json_response.last['note']).to eq("another comment on a MR") - end - - it "returns a 404 error if merge_request_iid is invalid" do - get api("/projects/#{project.id}/merge_requests/999/comments", user) - expect(response).to have_http_status(404) - end - - it "returns a 404 error if merge_request id is used instead of iid" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_http_status(404) - end - end - describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do it 'returns the issue that will be closed on merge' do issue = create(:issue, project: project) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 347f8f6fa3b..d8eb8ce921e 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -34,7 +34,7 @@ describe API::Notes, api: true do describe "GET /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do it "returns an array of issue notes" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -50,7 +50,7 @@ describe API::Notes, api: true do context "and current user cannot view the notes" do it "returns an empty array" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -62,7 +62,7 @@ describe API::Notes, api: true do before { ext_issue.update_attributes(confidential: true) } it "returns 404" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) expect(response).to have_http_status(404) end @@ -70,7 +70,7 @@ describe API::Notes, api: true do context "and current user can view the note" do it "returns an empty array" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -106,7 +106,7 @@ describe API::Notes, api: true do context "when noteable is a Merge Request" do it "returns an array of merge_requests notes" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -131,21 +131,21 @@ describe API::Notes, api: true do describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do context "when noteable is an Issue" do it "returns an issue note by id" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", user) expect(response).to have_http_status(200) expect(json_response['body']).to eq(issue_note.note) end it "returns a 404 error if issue note not found" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user) expect(response).to have_http_status(404) end context "and current user cannot view the note" do it "returns a 404 error" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user) expect(response).to have_http_status(404) end @@ -154,7 +154,7 @@ describe API::Notes, api: true do before { issue.update_attributes(confidential: true) } it "returns 404" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user) expect(response).to have_http_status(404) end @@ -162,7 +162,7 @@ describe API::Notes, api: true do context "and current user can view the note" do it "returns an issue note by id" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user) expect(response).to have_http_status(200) expect(json_response['body']).to eq(cross_reference_note.note) @@ -190,7 +190,7 @@ describe API::Notes, api: true do describe "POST /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do it "creates a new issue note" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' expect(response).to have_http_status(201) expect(json_response['body']).to eq('hi!') @@ -198,13 +198,13 @@ describe API::Notes, api: true do end it "returns a 400 bad request error if body not given" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user) expect(response).to have_http_status(400) end it "returns a 401 unauthorized error if user not authenticated" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes"), body: 'hi!' expect(response).to have_http_status(401) end @@ -212,7 +212,7 @@ describe API::Notes, api: true do context 'when an admin or owner makes the request' do it 'accepts the creation date to be set' do creation_time = 2.weeks.ago - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!', created_at: creation_time expect(response).to have_http_status(201) @@ -226,7 +226,7 @@ describe API::Notes, api: true do let(:issue2) { create(:issue, project: project) } it 'creates a new issue note' do - post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + post api("/projects/#{project.id}/issues/#{issue2.iid}/notes", user), body: ':+1:' expect(response).to have_http_status(201) expect(json_response['body']).to eq(':+1:') @@ -235,7 +235,7 @@ describe API::Notes, api: true do context 'when the user is posting an award emoji on his/her own issue' do it 'creates a new issue note' do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: ':+1:' expect(response).to have_http_status(201) expect(json_response['body']).to eq(':+1:') @@ -270,7 +270,7 @@ describe API::Notes, api: true do project = create(:empty_project, :private) { |p| p.add_guest(user) } issue = create(:issue, :confidential, project: project) - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'Foo' expect(response).to have_http_status(404) @@ -285,7 +285,7 @@ describe API::Notes, api: true do # from a different project, see #15577 # before do - post api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), + post api("/projects/#{private_issue.project.id}/issues/#{private_issue.iid}/notes", user), body: 'Hi!' end @@ -303,14 +303,14 @@ describe API::Notes, api: true do it "creates an activity event when an issue note is created" do expect(Event).to receive(:create) - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' end end describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'returns modified note' do - put api("/projects/#{project.id}/issues/#{issue.id}/"\ + put api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -318,14 +318,14 @@ describe API::Notes, api: true do end it 'returns a 404 error when note id not found' do - put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), + put api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user), body: 'Hello!' expect(response).to have_http_status(404) end it 'returns a 400 bad request error if body not given' do - put api("/projects/#{project.id}/issues/#{issue.id}/"\ + put api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(400) @@ -351,7 +351,7 @@ describe API::Notes, api: true do context 'when noteable is a Merge Request' do it 'returns modified note' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -359,7 +359,7 @@ describe API::Notes, api: true do end it 'returns a 404 error when note id not found' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\ "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -370,18 +370,18 @@ describe API::Notes, api: true do describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'deletes a note' do - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + delete api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(204) # Check if note is really deleted - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + delete api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do - delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user) expect(response).to have_http_status(404) end @@ -410,18 +410,18 @@ describe API::Notes, api: true do context 'when noteable is a Merge Request' do it 'deletes a note' do delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.iid}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.iid}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) + "#{merge_request.iid}/notes/12345", user) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c481b7e72b1..a3de4702ad0 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -902,7 +902,7 @@ describe API::Projects, :api do end end - describe :fork_admin do + describe 'fork management' do let(:project_fork_target) { create(:empty_project) } let(:project_fork_source) { create(:empty_project, :public) } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 442b2df1952..044b989e5ba 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -152,6 +152,34 @@ describe API::Runner do end end end + + describe 'POST /api/v4/runners/verify' do + let(:runner) { create(:ci_runner) } + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners/verify') + + expect(response).to have_http_status :bad_request + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners/verify'), token: 'invalid-token' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'verifies Runner credentials' do + post api('/runners/verify'), token: runner.token + + expect(response).to have_http_status 200 + end + end + end end describe '/api/v4/jobs' do @@ -220,18 +248,6 @@ describe API::Runner do it { expect(response).to have_http_status(204) } end end - - context "when runner doesn't send version in User-Agent" do - let(:user_agent) { 'Go-http-client/1.1' } - - it { expect(response).to have_http_status(404) } - end - - context "when runner doesn't have a User-Agent" do - let(:user_agent) { nil } - - it { expect(response).to have_http_status(404) } - end end context 'when no token is provided' do @@ -254,10 +270,10 @@ describe API::Runner do context 'when Runner is not active' do let(:runner) { create(:ci_runner, :inactive) } - it 'returns 404 error' do + it 'returns 204 error' do request_job - expect(response).to have_http_status 404 + expect(response).to have_http_status 204 end end @@ -401,9 +417,39 @@ describe API::Runner do end context 'when project and pipeline have multiple jobs' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - before { job.success } + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(2) + expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when explicit dependencies are defined' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + stage: 'deploy', stage_idx: 1, + options: { dependencies: [job2.name] }) + end + + before do + job.success + job2.success + end it 'returns dependent jobs' do request_job @@ -411,7 +457,7 @@ describe API::Runner do expect(response).to have_http_status(201) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(1) - expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach') + expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) end end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 424c02932ab..d93a734f5b6 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -59,14 +59,6 @@ describe API::Triggers do expect(pipeline.builds.size).to eq(5) end - it 'creates builds on webhook from other gitlab repository and branch' do - expect do - post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - - expect(response).to have_http_status(201) - end - it 'returns bad request with no pipeline created if there\'s no commit for that ref' do post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') @@ -101,6 +93,28 @@ describe API::Triggers do end end end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + + expect(response).to have_http_status(201) + end + end + end end describe 'GET /projects/:id/triggers' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 04e7837fd7a..f793c0db2f3 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -676,7 +676,7 @@ describe API::Users, api: true do before { admin } it "deletes user" do - delete api("/users/#{user.id}", admin) + Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) } expect(response).to have_http_status(204) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound @@ -684,23 +684,23 @@ describe API::Users, api: true do end it "does not delete for unauthenticated user" do - delete api("/users/#{user.id}") + Sidekiq::Testing.inline! { delete api("/users/#{user.id}") } expect(response).to have_http_status(401) end it "is not available for non admin users" do - delete api("/users/#{user.id}", user) + Sidekiq::Testing.inline! { delete api("/users/#{user.id}", user) } expect(response).to have_http_status(403) end it "returns 404 for non-existing user" do - delete api("/users/999999", admin) + Sidekiq::Testing.inline! { delete api("/users/999999", admin) } expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - delete api("/users/ASDF", admin) + Sidekiq::Testing.inline! { delete api("/users/ASDF", admin) } expect(response).to have_http_status(404) end diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 1941ca0d7d8..383871d5c38 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -285,8 +285,16 @@ describe API::V3::Issues, api: true do end let(:base_url) { "/groups/#{group.id}/issues" } + it 'returns all group issues (including opened and closed)' do + get v3_api(base_url, admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + end + it 'returns group issues without confidential issues for non project members' do - get v3_api(base_url, non_member) + get v3_api("#{base_url}?state=opened", non_member) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -295,7 +303,7 @@ describe API::V3::Issues, api: true do end it 'returns group confidential issues for author' do - get v3_api(base_url, author) + get v3_api("#{base_url}?state=opened", author) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -303,7 +311,7 @@ describe API::V3::Issues, api: true do end it 'returns group confidential issues for assignee' do - get v3_api(base_url, assignee) + get v3_api("#{base_url}?state=opened", assignee) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -311,7 +319,7 @@ describe API::V3::Issues, api: true do end it 'returns group issues with confidential issues for project members' do - get v3_api(base_url, user) + get v3_api("#{base_url}?state=opened", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -319,7 +327,7 @@ describe API::V3::Issues, api: true do end it 'returns group confidential issues for admin' do - get v3_api(base_url, admin) + get v3_api("#{base_url}?state=opened", admin) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -368,7 +376,7 @@ describe API::V3::Issues, api: true do end it 'returns an array of issues in given milestone' do - get v3_api("#{base_url}?milestone=#{group_milestone.title}", user) + get v3_api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -439,6 +447,12 @@ describe API::V3::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get v3_api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index d8bb562587d..b1aa793ec00 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -949,7 +949,7 @@ describe API::V3::Projects, api: true do end end - describe :fork_admin do + describe 'fork management' do let(:project_fork_target) { create(:empty_project) } let(:project_fork_source) { create(:empty_project, :public) } diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index 4819269d69f..9233e9621bf 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -51,13 +51,6 @@ describe API::V3::Triggers do expect(pipeline.builds.size).to eq(5) end - it 'creates builds on webhook from other gitlab repository and branch' do - expect do - post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - expect(response).to have_http_status(201) - end - it 'returns bad request with no builds created if there\'s no commit for that ref' do post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) @@ -89,6 +82,27 @@ describe API::V3::Triggers do end end end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + + expect(response).to have_http_status(201) + end + end + end end describe 'GET /projects/:id/triggers' do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 17bbb0b53c1..b38cbe74b85 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -263,4 +263,18 @@ describe API::V3::Users, api: true do expect(json_response['message']).to eq('404 User Not Found') end end + + describe 'POST /users' do + it 'creates confirmed user when confirm parameter is false' do + optional_attributes = { confirm: false } + attributes = attributes_for(:user).merge(optional_attributes) + + post v3_api('/users', admin), attributes + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).to be_confirmed + end + end end diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb new file mode 100644 index 00000000000..ba124de70bb --- /dev/null +++ b/spec/routing/environments_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Projects::EnvironmentsController, :routing do + let(:project) { create(:empty_project) } + + let(:environment) do + create(:environment, project: project, + name: 'staging-1.0/review') + end + + let(:environments_route) do + "#{project.namespace.name}/#{project.name}/environments/" + end + + describe 'routing environment folders' do + context 'when using JSON format' do + it 'correctly matches environment name and JSON format' do + expect(get_folder('staging-1.0.json')) + .to route_to(*folder_action(id: 'staging-1.0', format: 'json')) + end + end + + context 'when using HTML format' do + it 'correctly matches environment name and HTML format' do + expect(get_folder('staging-1.0.html')) + .to route_to(*folder_action(id: 'staging-1.0', format: 'html')) + end + end + + context 'when using implicit format' do + it 'correctly matches environment name' do + expect(get_folder('staging-1.0')) + .to route_to(*folder_action(id: 'staging-1.0')) + end + end + end + + def get_folder(folder) + get("#{project.namespace.name}/#{project.name}/" \ + "environments/folders/#{folder}") + end + + def folder_action(**opts) + options = { namespace_id: project.namespace.name, + project_id: project.name } + + ['projects/environments#folder', options.merge(opts)] + end +end diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb index 2f08958a783..ba24cf8e481 100644 --- a/spec/serializers/analytics_issue_serializer_spec.rb +++ b/spec/serializers/analytics_issue_serializer_spec.rb @@ -8,7 +8,7 @@ describe AnalyticsIssueSerializer do end let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:resource) do { total_time: "172802.724419", diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb index 62067cc0ef2..56cb08acfc6 100644 --- a/spec/serializers/analytics_merge_request_serializer_spec.rb +++ b/spec/serializers/analytics_merge_request_serializer_spec.rb @@ -8,7 +8,7 @@ describe AnalyticsMergeRequestSerializer do end let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:resource) do { total_time: "172802.724419", diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index eed957fa5ef..f76a5cf72d1 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' describe BuildEntity do + let(:user) { create(:user) } let(:build) { create(:ci_build) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end let(:entity) do - described_class.new(build, request: double) + described_class.new(build, request: request) end subject { entity.as_json } @@ -26,6 +32,11 @@ describe BuildEntity do expect(subject).to include(:created_at, :updated_at) end + it 'contains details' do + expect(subject).to include :status + expect(subject[:status]).to include :icon, :favicon, :text, :label + end + context 'when build is a regular job' do it 'does not contain path to play action' do expect(subject).not_to include(:play_path) diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb new file mode 100644 index 00000000000..3cc791bca50 --- /dev/null +++ b/spec/serializers/build_serializer_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe BuildSerializer do + let(:user) { create(:user) } + + let(:serializer) do + described_class.new(user: user) + end + + subject { serializer.represent(resource) } + + describe '#represent' do + context 'when a single object is being serialized' do + let(:resource) { create(:ci_build) } + + it 'serializers the pipeline object' do + expect(subject[:id]).to eq resource.id + end + end + + context 'when multiple objects are being serialized' do + let(:resource) { create_list(:ci_build, 2) } + + it 'serializers the array of pipelines' do + expect(subject).not_to be_empty + end + end + end + + describe '#represent_status' do + context 'when represents only status' do + let(:resource) { create(:ci_build) } + let(:status) { resource.detailed_status(double('user')) } + + subject { serializer.represent_status(resource) } + + it 'serializes only status' do + expect(subject[:text]).to eq(status.text) + expect(subject[:label]).to eq(status.label) + expect(subject[:icon]).to eq(status.icon) + expect(subject[:favicon]).to eq(status.favicon) + end + end + end +end diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb index 0333d73b5b5..04247c78549 100644 --- a/spec/serializers/commit_entity_spec.rb +++ b/spec/serializers/commit_entity_spec.rb @@ -6,7 +6,7 @@ describe CommitEntity do end let(:request) { double('request') } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:commit) { project.commit } subject { entity.as_json } diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index ea87771e2a2..95eca5463eb 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -1,8 +1,15 @@ require 'spec_helper' describe DeploymentEntity do + let(:user) { create(:user) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end + let(:entity) do - described_class.new(deployment, request: double) + described_class.new(deployment, request: request) end let(:deployment) { create(:deployment) } diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index 57728ce3181..979d9921941 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -15,4 +15,24 @@ describe EnvironmentEntity do it 'exposes core elements of environment' do expect(subject).to include(:id, :name, :state, :environment_path) end + + context 'metrics disabled' do + before do + allow(environment).to receive(:has_metrics?).and_return(false) + end + + it "doesn't expose metrics path" do + expect(subject).not_to include(:metrics_path) + end + end + + context 'metrics enabled' do + before do + allow(environment).to receive(:has_metrics?).and_return(true) + end + + it 'exposes metrics path' do + expect(subject).to include(:metrics_path) + end + end end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 6a6df377b35..1909e6385b5 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe EnvironmentSerializer do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:json) do described_class @@ -11,21 +11,20 @@ describe EnvironmentSerializer do end context 'when there is a single object provided' do - before do - create(:ci_build, :manual, name: 'manual1', - pipeline: deployable.pipeline) - end - + let(:project) { create(:project, :repository) } + let(:deployable) { create(:ci_build) } let(:deployment) do create(:deployment, deployable: deployable, user: user, project: project, sha: project.commit.id) end - - let(:deployable) { create(:ci_build) } let(:resource) { deployment.environment } + before do + create(:ci_build, :manual, name: 'manual1', pipeline: deployable.pipeline) + end + it 'contains important elements of environment' do expect(json) .to include(:name, :external_url, :environment_path, :last_deployment) diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index ccb72973f9c..93d5a21419d 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -30,7 +30,7 @@ describe PipelineEntity do .to include :duration, :finished_at expect(subject[:details]) .to include :stages, :artifacts, :manual_actions - expect(subject[:details][:status]).to include :icon, :text, :label + expect(subject[:details][:status]).to include :icon, :favicon, :text, :label end it 'contains flags' do diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 2aaef03cb93..8642b803844 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -94,4 +94,20 @@ describe PipelineSerializer do end end end + + describe '#represent_status' do + context 'when represents only status' do + let(:resource) { create(:ci_pipeline) } + let(:status) { resource.detailed_status(double('user')) } + + subject { serializer.represent_status(resource) } + + it 'serializes only status' do + expect(subject[:text]).to eq(status.text) + expect(subject[:label]).to eq(status.label) + expect(subject[:icon]).to eq(status.icon) + expect(subject[:favicon]).to eq(status.favicon) + end + end + end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 89428b4216e..c94902dbab8 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -16,7 +16,7 @@ describe StatusEntity do subject { entity.as_json } it 'contains status details' do - expect(subject).to include :text, :icon, :label, :group + expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path end end diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb index d29e0addb53..77ca17bc82c 100644 --- a/spec/services/after_branch_delete_service_spec.rb +++ b/spec/services/after_branch_delete_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe AfterBranchDeleteService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index 7b29b043296..a8555f5b4a0 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -15,7 +15,7 @@ describe Boards::CreateService, services: true do board = service.execute expect(board.lists.size).to eq 1 - expect(board.lists.first).to be_done + expect(board.lists.first).to be_closed end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 22115c6566d..c982031c791 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -15,7 +15,7 @@ describe Boards::Issues::ListService, services: true do let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } - let!(:done) { create(:done_list, board: board) } + let!(:closed) { create(:closed_list, board: board) } let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } @@ -30,6 +30,7 @@ describe Boards::Issues::ListService, services: true do let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } let!(:closed_issue3) { create(:issue, :closed, project: project) } let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } + let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) } before do project.team << [user, :developer] @@ -52,12 +53,12 @@ describe Boards::Issues::ListService, services: true do expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] end - it 'returns closed issues when listing issues from Done' do - params = { board_id: board.id, id: done.id } + it 'returns closed issues when listing issues from Closed' do + params = { board_id: board.id, id: closed.id } issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] end it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 727ea04ea5c..4ff7ac6bb2f 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -12,7 +12,7 @@ describe Boards::Issues::MoveService, services: true do let!(:list1) { create(:list, board: board1, label: development, position: 0) } let!(:list2) { create(:list, board: board1, label: testing, position: 1) } - let!(:done) { create(:done_list, board: board1) } + let!(:closed) { create(:closed_list, board: board1) } before do project.team << [user, :developer] @@ -35,13 +35,13 @@ describe Boards::Issues::MoveService, services: true do end end - context 'when moving to done' do + context 'when moving to closed' do let(:board2) { create(:board, project: project) } let(:regression) { create(:label, project: project, name: 'Regression') } let!(:list3) { create(:list, board: board2, label: regression, position: 1) } let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) } - let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: done.id } } + let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.id } } it 'delegates the close proceedings to Issues::CloseService' do expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once @@ -58,9 +58,9 @@ describe Boards::Issues::MoveService, services: true do end end - context 'when moving from done' do + context 'when moving from closed' do let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) } - let(:params) { { board_id: board1.id, from_list_id: done.id, to_list_id: list2.id } } + let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } } it 'delegates the re-open proceedings to Issues::ReopenService' do expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index a30860f828a..af2d7c784bb 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -18,18 +18,18 @@ describe Boards::Lists::DestroyService, services: true do development = create(:list, board: board, position: 0) review = create(:list, board: board, position: 1) staging = create(:list, board: board, position: 2) - done = board.done_list + closed = board.closed_list described_class.new(project, user).execute(development) expect(review.reload.position).to eq 0 expect(staging.reload.position).to eq 1 - expect(done.reload.position).to be_nil + expect(closed.reload.position).to be_nil end end - it 'does not remove list from board when list type is done' do - list = board.done_list + it 'does not remove list from board when list type is closed' do + list = board.closed_list service = described_class.new(project, user) expect { service.execute(list) }.not_to change(board.lists, :count) diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index 2dffc62b215..ab9fb1bc914 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do service = described_class.new(project, double) - expect(service.execute(board)).to eq [list, board.done_list] + expect(service.execute(board)).to eq [list, board.closed_list] end end end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 3786dc82bf0..4b3bdd133f2 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::MoveService, services: true do let!(:development) { create(:list, board: board, position: 1) } let!(:review) { create(:list, board: board, position: 2) } let!(:staging) { create(:list, board: board, position: 3) } - let!(:done) { create(:done_list, board: board) } + let!(:closed) { create(:closed_list, board: board) } context 'when list type is set to label' do it 'keeps position of lists when new position is nil' do @@ -86,10 +86,10 @@ describe Boards::Lists::MoveService, services: true do end end - it 'keeps position of lists when list type is done' do + it 'keeps position of lists when list type is closed' do service = described_class.new(project, user, position: 2) - service.execute(done) + service.execute(closed) expect(current_list_positions).to eq [0, 1, 2, 3] end diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index 51441e8f3be..0dc96521fa8 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -18,9 +18,16 @@ describe ChatNames::FindUserService, services: true do end it 'updates when last time chat name was used' do + expect(chat_name.last_used_at).to be_nil + subject - expect(chat_name.reload.last_used_at).to be_like_time(Time.now) + initial_last_used = chat_name.reload.last_used_at + expect(initial_last_used).to be_present + + Timecop.travel(2.days.from_now) { described_class.new(service, params).execute } + + expect(chat_name.reload.last_used_at).to be > initial_last_used end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index a969829a63e..d2f0337c260 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreatePipelineService, services: true do - let(:project) { FactoryGirl.create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:admin) } before do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 5e68343784d..5a20102872a 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do let(:service) { described_class.new } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:trigger) { create(:ci_trigger, project: project) } before do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 65af4e13118..8567817147b 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -19,7 +19,7 @@ describe Ci::RetryBuildService, :services do erased_at].freeze IGNORE_ACCESSORS = - %i[type lock_version target_url gl_project_id deploy job_id base_tags + %i[type lock_version target_url base_tags commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id user_id].freeze diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 5445b65f4e8..f1b2d3a4798 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -9,6 +9,19 @@ describe Ci::RetryPipelineService, '#execute', :services do context 'when user has ability to modify pipeline' do let(:user) { create(:admin) } + context 'when there are already retried jobs present' do + before do + create_build('rspec', :canceled, 0) + create_build('rspec', :failed, 0) + end + + it 'does not retry jobs that has already been retried' do + expect(statuses.first).to be_retried + expect { service.execute(pipeline) } + .to change { CommitStatus.count }.by(1) + end + end + context 'when there are failed builds in the last stage' do before do create_build('rspec 1', :success, 0) diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 560f83d94f7..32c72a9cf5e 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::StopEnvironmentsService, services: true do - let(:project) { create(:project, :private) } + let(:project) { create(:project, :private, :repository) } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index f01a388b895..c44e6b2a48b 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::UpdateBuildQueueService, :services do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_pipeline, project: project) } diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb index 0a7fc58523f..bea7c965233 100644 --- a/spec/services/compare_service_spec.rb +++ b/spec/services/compare_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe CompareService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class.new(project, 'feature') } diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb new file mode 100644 index 00000000000..3f548688c20 --- /dev/null +++ b/spec/services/create_branch_service_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe CreateBranchService, services: true do + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + context 'when repository is empty' do + let(:project) { create(:project_empty_repo) } + + it 'creates master branch' do + service.execute('my-feature', 'master') + + expect(project.repository.branch_exists?('master')).to be_truthy + end + + it 'creates my-feature branch' do + service.execute('my-feature', 'master') + + expect(project.repository.branch_exists?('my-feature')).to be_truthy + end + end + end +end diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb index 61e5ae72f51..271ccfe7968 100644 --- a/spec/services/create_release_service_spec.rb +++ b/spec/services/create_release_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe CreateReleaseService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb index 336f5dafb5b..c4685c9aa31 100644 --- a/spec/services/delete_branch_service_spec.rb +++ b/spec/services/delete_branch_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe DeleteBranchService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index 181488e89c7..a41a421fa6e 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DeleteMergedBranchesService, services: true do subject(:service) { described_class.new(project, project.owner) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } context '#execute' do context 'unprotected branches' do diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 35e6e139238..26aa5b432d4 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Files::UpdateService do subject { described_class.new(project, user, commit_params) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } let(:new_contents) { 'New Content' } diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 3318dfb22b6..ac7ccfbaab0 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe GitHooksService, services: true do include RepoHelpers - let(:user) { create :user } - let(:project) { create :project } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } let(:service) { GitHooksService.new } before do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index bd71618e6f4..0477cac6677 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe GitPushService, services: true do include RepoHelpers - let(:user) { create :user } - let(:project) { create :project } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } before do project.team << [user, :master] diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index bd074b9bd71..b73beb3f6fc 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe GitTagPushService, services: true do include RepoHelpers - let(:user) { create :user } - let(:project) { create :project } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } let(:oldrev) { Gitlab::Git::BLANK_SHA } diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index ec89b540e6a..bcb62429275 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -44,7 +44,7 @@ describe Groups::CreateService, '#execute', services: true do let!(:service) { described_class.new(user, params) } before do - Settings.mattermost['enabled'] = true + stub_mattermost_setting(enabled: true) end it 'create the chat team with the group' do diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 98c560ffb26..2ee11fc8b4c 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -6,7 +6,7 @@ describe Groups::DestroyService, services: true do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } - let!(:project) { create(:project, namespace: group) } + let!(:project) { create(:empty_project, namespace: group) } let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 7c0fccb9d41..f6ad5cebd2c 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -13,7 +13,7 @@ describe Groups::UpdateService, services: true do before do public_group.add_user(user, Gitlab::Access::MASTER) - create(:project, :public, group: public_group) + create(:empty_project, :public, group: public_group) end it "does not change permission level" do @@ -27,7 +27,7 @@ describe Groups::UpdateService, services: true do before do internal_group.add_user(user, Gitlab::Access::MASTER) - create(:project, :internal, group: internal_group) + create(:empty_project, :internal, group: internal_group) end it "does not change permission level" do @@ -36,6 +36,20 @@ describe Groups::UpdateService, services: true do end end end + + context "with parent_id user doesn't have permissions for" do + let(:service) { described_class.new(public_group, user, parent_id: private_group.id) } + + before do + service.execute + end + + it 'does not update parent_id' do + updated_group = public_group.reload + + expect(updated_group.parent_id).to be_nil + end + end end context "unauthorized visibility_level validation" do @@ -55,7 +69,7 @@ describe Groups::UpdateService, services: true do before do internal_group.add_user(user, Gitlab::Access::MASTER) - create(:project, :internal, group: internal_group) + create(:empty_project, :internal, group: internal_group) end it 'returns true' do diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 0475f38fe5e..7a1ac027310 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -138,7 +138,7 @@ describe Issuable::BulkUpdateService, services: true do let(:labels) { [bug, regression] } it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) end it 'does not update issues not passed in' do diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 1dd53236fbd..17990f41b3b 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper.rb' describe Issues::BuildService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } before do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index db196ed5751..9f8346d52bb 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -5,8 +5,8 @@ describe Issues::MoveService, services: true do let(:author) { create(:user) } let(:title) { 'Some issue' } let(:description) { 'Some issue description' } - let(:old_project) { create(:project) } - let(:new_project) { create(:project) } + let(:old_project) { create(:empty_project) } + let(:new_project) { create(:empty_project) } let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') } let(:old_issue) do diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 6cc738aec08..3a72f92383c 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -10,7 +10,7 @@ class DummyService < Issues::BaseService end describe DummyService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } before do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index fa472f3e2c3..5b324f3c706 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -13,6 +13,7 @@ describe Issues::UpdateService, services: true do let(:issue) do create(:issue, title: 'Old title', + description: "for #{user2.to_reference}", assignee_id: user3.id, project: project) end @@ -182,16 +183,24 @@ describe Issues::UpdateService, services: true do it 'marks pending todos as done' do expect(todo.reload.done?).to eq true end + + it 'does not create any new todos' do + expect(Todo.count).to eq(1) + end end context 'when the description change' do before do - update_issue(description: 'Also please fix') + update_issue(description: "Also please fix #{user2.to_reference} #{user3.to_reference}") end it 'marks todos as done' do expect(todo.reload.done?).to eq true end + + it 'creates only 1 new todo' do + expect(Todo.count).to eq(2) + end end context 'when is reassigned' do diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb new file mode 100644 index 00000000000..0ecab0c3475 --- /dev/null +++ b/spec/services/labels/create_service_spec.rb @@ -0,0 +1,186 @@ +require 'spec_helper' + +describe Labels::CreateService, services: true do + describe '#execute' do + let(:project) { create(:project) } + let(:group) { create(:group) } + + let(:hex_color) { '#FF0000' } + let(:named_color) { 'red' } + let(:upcase_color) { 'RED' } + let(:spaced_color) { ' red ' } + let(:unknown_color) { 'unknown' } + let(:no_color) { '' } + + let(:expected_saved_color) { hex_color } + + context 'in a project' do + context 'with color in hex-code' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(hex_color)).execute(project: project) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(named_color)).execute(project: project) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in up-case allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(upcase_color)).execute(project: project) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color surrounded by spaces' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(spaced_color)).execute(project: project) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with unknown color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(unknown_color)).execute(project: project) + + expect(label).not_to be_persisted + end + end + + context 'with no color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(no_color)).execute(project: project) + + expect(label).not_to be_persisted + end + end + end + + context 'in a group' do + context 'with color in hex-code' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(hex_color)).execute(group: group) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(named_color)).execute(group: group) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in up-case allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(upcase_color)).execute(group: group) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color surrounded by spaces' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(spaced_color)).execute(group: group) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with unknown color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(unknown_color)).execute(group: group) + + expect(label).not_to be_persisted + end + end + + context 'with no color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(no_color)).execute(group: group) + + expect(label).not_to be_persisted + end + end + end + + context 'in admin area' do + context 'with color in hex-code' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(hex_color)).execute(template: true) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(named_color)).execute(template: true) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color in up-case allowed name' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(upcase_color)).execute(template: true) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with color surrounded by spaces' do + it 'creates a label' do + label = Labels::CreateService.new(params_with(spaced_color)).execute(template: true) + + expect(label).to be_persisted + expect(label.color).to eq expected_saved_color + end + end + + context 'with unknown color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(unknown_color)).execute(template: true) + + expect(label).not_to be_persisted + end + end + + context 'with no color' do + it 'doesn\'t create a label' do + label = Labels::CreateService.new(params_with(no_color)).execute(template: true) + + expect(label).not_to be_persisted + end + end + end + end + + def params_with(color) + { + title: 'A Label', + color: color + } + end +end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index 7a9b34f9f96..de8f1827cce 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Labels::FindOrCreateService, services: true do describe '#execute' do let(:group) { create(:group) } - let(:project) { create(:project, namespace: group) } + let(:project) { create(:empty_project, namespace: group) } let(:params) do { diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index 13654a0881c..11d5f1cad5e 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -6,8 +6,8 @@ describe Labels::TransferService, services: true do let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } - let(:project_1) { create(:project, namespace: group_2) } - let(:project_2) { create(:project, namespace: group_3) } + let(:project_1) { create(:empty_project, namespace: group_2) } + let(:project_2) { create(:empty_project, namespace: group_3) } let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') } let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') } diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb new file mode 100644 index 00000000000..f2498ea6990 --- /dev/null +++ b/spec/services/labels/update_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Labels::UpdateService, services: true do + describe '#execute' do + let(:project) { create(:project) } + + let(:hex_color) { '#FF0000' } + let(:named_color) { 'red' } + let(:upcase_color) { 'RED' } + let(:spaced_color) { ' red ' } + let(:unknown_color) { 'unknown' } + let(:no_color) { '' } + + let(:expected_saved_color) { hex_color } + + before(:each) do + @label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project) + expect(@label).to be_persisted + end + + context 'with color in hex-code' do + it 'updates the label' do + label = Labels::UpdateService.new(params_with(hex_color)).execute(@label) + + expect(label).to be_valid + expect(label.reload.color).to eq expected_saved_color + end + end + + context 'with color in allowed name' do + it 'updates the label' do + label = Labels::UpdateService.new(params_with(named_color)).execute(@label) + + expect(label).to be_valid + expect(label.reload.color).to eq expected_saved_color + end + end + + context 'with color in up-case allowed name' do + it 'updates the label' do + label = Labels::UpdateService.new(params_with(upcase_color)).execute(@label) + + expect(label).to be_valid + expect(label.reload.color).to eq expected_saved_color + end + end + + context 'with color surrounded by spaces' do + it 'updates the label' do + label = Labels::UpdateService.new(params_with(spaced_color)).execute(@label) + + expect(label).to be_valid + expect(label.reload.color).to eq expected_saved_color + end + end + + context 'with unknown color' do + it 'doesn\'t update the label' do + label = Labels::UpdateService.new(params_with(unknown_color)).execute(@label) + + expect(label).not_to be_valid + end + end + + context 'with no color' do + it 'doesn\'t update the label' do + label = Labels::UpdateService.new(params_with(no_color)).execute(@label) + + expect(label).not_to be_valid + end + end + end + + def params_with(color) + { + title: 'A Label', + color: color + } + end +end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 574df6e0f42..41450c67d7e 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Members::DestroyService, services: true do let(:user) { create(:user) } let(:member_user) { create(:user) } - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:group) { create(:group, :public) } shared_examples 'a service raising ActiveRecord::RecordNotFound' do diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb index 853c125dadb..814c17b9ad0 100644 --- a/spec/services/members/request_access_service_spec.rb +++ b/spec/services/members/request_access_service_spec.rb @@ -29,7 +29,7 @@ describe Members::RequestAccessService, services: true do end context 'when current user cannot request access to the project' do - %i[project group].each do |source_type| + %i[empty_project group].each do |source_type| it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { create(source_type, :private) } end @@ -37,7 +37,7 @@ describe Members::RequestAccessService, services: true do end context 'when access requests are disabled' do - %i[project group].each do |source_type| + %i[empty_project group].each do |source_type| it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { create(source_type, :public) } end @@ -45,7 +45,7 @@ describe Members::RequestAccessService, services: true do end context 'when current user can request access to the project' do - %i[project group].each do |source_type| + %i[empty_project group].each do |source_type| it_behaves_like 'a service creating a access request' do let(:source) { create(source_type, :public, :access_requestable) } end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index d80fb8a1af1..af0a214c00f 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::AddTodoWhenBuildFailsService do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:sha) { '1234567890abcdef1234567890abcdef12345678' } let(:ref) { merge_request.source_branch } diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index 5034b6ef33f..fe75757dd29 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe MergeRequests::AssignIssuesService, services: true do let(:user) { create(:user) } - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public, :repository) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") } let(:service) { described_class.new(project, user, merge_request: merge_request) } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 0768f644036..c8bd4d4601a 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::BuildService, services: true do include RepoHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:issue_confidential) { false } let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) } @@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do let(:commits) { Commit.decorate([commit_1], project) } it 'creates compare object with target branch as default branch' do - expect(merge_request.can_be_created).to eq(false) expect(merge_request.compare).to be_present expect(merge_request.target_branch).to eq(project.default_branch) end + + it 'allows the merge request to be created' do + expect(merge_request.can_be_created).to eq(true) + end end context 'same source and target branch' do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 673c0bd6c9c..0e16c7cc94b 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe MergeRequests::CreateService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:assignee) { create(:user) } diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index b7a05907208..290e00ea1ba 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe MergeRequests::GetUrlsService do - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public, :repository) } let(:service) { MergeRequests::GetUrlsService.new(project) } let(:source_branch) { "my_branch" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index c2f205c389d..769b3193275 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe MergeRequests::MergeWhenPipelineSucceedsService do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:mr_merge_if_green_enabled) do create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 92729f68e5f..c22d145ca5d 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe MergeRequests::RefreshService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { MergeRequests::RefreshService } @@ -11,7 +11,7 @@ describe MergeRequests::RefreshService, services: true do group = create(:group) group.add_owner(@user) - @project = create(:project, namespace: group) + @project = create(:project, :repository, namespace: group) @fork_project = Projects::ForkService.new(@project, @user).execute @merge_request = create(:merge_request, source_project: @project, @@ -252,7 +252,7 @@ describe MergeRequests::RefreshService, services: true do context 'when the merge request is sourced from a different project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - forked_project = create(:project) + forked_project = create(:project, :repository) create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project) merge_request = create(:merge_request, diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index d33535d22af..eaf7785e549 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe MergeRequests::ResolveService do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:fork_project) do create(:forked_project_with_submodules) do |fork_project| diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 7d73c0ea5d0..f2ca1e6fcbd 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::UpdateService, services: true do include EmailHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -12,6 +12,7 @@ describe MergeRequests::UpdateService, services: true do let(:merge_request) do create(:merge_request, :simple, title: 'Old title', + description: "FYI #{user2.to_reference}", assignee_id: user3.id, source_project: project) end @@ -225,16 +226,24 @@ describe MergeRequests::UpdateService, services: true do it 'marks pending todos as done' do expect(pending_todo.reload).to be_done end + + it 'does not create any new todos' do + expect(Todo.count).to eq(1) + end end context 'when the description change' do before do - update_merge_request({ description: 'Also please fix' }) + update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" }) end it 'marks pending todos as done' do expect(pending_todo.reload).to be_done end + + it 'creates only 1 new todo' do + expect(Todo.count).to eq(2) + end end context 'when is reassigned' do diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 92b84308f73..d581b94ff43 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Milestones::CloseService, services: true do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } before do @@ -17,7 +17,7 @@ describe Milestones::CloseService, services: true do it { expect(milestone).to be_valid } it { expect(milestone).to be_closed } - describe :event do + describe 'event' do let(:event) { Event.recent.first } it { expect(event.milestone).to be_truthy } diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb new file mode 100644 index 00000000000..39f06f8f8e7 --- /dev/null +++ b/spec/services/note_summary_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe NoteSummary, services: true do + let(:project) { build(:empty_project) } + let(:noteable) { build(:issue) } + let(:user) { build(:user) } + + def create_note_summary + described_class.new(noteable, project, user, 'note', action: 'icon', commit_count: 5) + end + + describe '#metadata?' do + it 'returns true when metadata present' do + expect(create_note_summary.metadata?).to be_truthy + end + + it 'returns false when metadata not present' do + expect(described_class.new(noteable, project, user, 'note').metadata?).to be_falsey + end + end + + describe '#note' do + it 'returns note hash' do + expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note') + end + + context 'when noteable is a commit' do + let(:noteable) { build(:commit) } + + it 'returns note hash specific to commit' do + expect(create_note_summary.note).to eq( + noteable: nil, project: project, author: user, note: 'note', + noteable_type: 'Commit', commit_id: noteable.id + ) + end + end + end + + describe '#metadata' do + it 'returns metadata hash' do + expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5) + end + end +end diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb index 110efb54fa0..d73ae51fbc3 100644 --- a/spec/services/notes/diff_position_update_service_spec.rb +++ b/spec/services/notes/diff_position_update_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Notes::DiffPositionUpdateService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") } let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb index dde4bde7dc2..905e2f46bde 100644 --- a/spec/services/notes/update_service_spec.rb +++ b/spec/services/notes/update_service_spec.rb @@ -4,12 +4,14 @@ describe Notes::UpdateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:user2) { create(:user) } + let(:user3) { create(:user) } let(:issue) { create(:issue, project: project) } - let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') } + let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") } before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [user3, :developer] end describe '#execute' do @@ -23,22 +25,30 @@ describe Notes::UpdateService, services: true do context 'when the note change' do before do - update_note({ note: 'New note' }) + update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" }) end it 'marks todos as done' do expect(todo.reload).to be_done end + + it 'creates only 1 new todo' do + expect(Todo.count).to eq(2) + end end context 'when the note does not change' do before do - update_note({ note: 'Old note' }) + update_note({ note: "Old note #{user2.to_reference}" }) end it 'keep todos' do expect(todo.reload).to be_pending end + + it 'does not create any new todos' do + expect(Todo.count).to eq(1) + end end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 82a4ec3f581..5c841843b40 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) @@ -362,7 +372,7 @@ describe NotificationService, services: true do end context 'commit note' do - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public, :repository) } let(:note) { create(:note_on_commit, project: project) } before do @@ -411,7 +421,7 @@ describe NotificationService, services: true do end context "merge request diff note" do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } @@ -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) @@ -758,7 +795,7 @@ describe NotificationService, services: true do update_custom_notification(:reopen_issue, @u_custom_global) end - it 'sends email to issue assignee and issue author' do + it 'sends email to issue notification recipients' do notification.reopen_issue(issue, @u_disabled) should_email(issue.assignee) @@ -772,6 +809,7 @@ describe NotificationService, services: true do should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) + should_not_email(@u_disabled) should_not_email(@u_lazy_participant) end @@ -781,11 +819,37 @@ describe NotificationService, services: true do let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } end end + + describe '#issue_moved' do + let(:new_issue) { create(:issue) } + + it 'sends email to issue notification recipients' do + notification.issue_moved(issue, new_issue, @u_disabled) + + should_email(issue.assignee) + should_email(issue.author) + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@watcher_and_subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@u_lazy_participant) + end + + it_behaves_like 'participating notifications' do + let(:participant) { create(:user, username: 'user-participant') } + let(:issuable) { issue } + let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } + end + end end describe 'Merge Requests' do let(:group) { create(:group) } - let(:project) { create(:project, :public, namespace: group) } + let(:project) { create(:project, :public, :repository, namespace: group) } let(:another_project) { create(:empty_project, :public, namespace: group) } let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' } @@ -818,6 +882,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 +1091,14 @@ describe NotificationService, services: true do should_not_email(@u_watcher) end + it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do + merge_request.merge_when_pipeline_succeeds = false + @u_watcher.notified_of_own_activity = true + notification.merge_mr(merge_request, @u_watcher) + + should_email(@u_watcher) + end + it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } @@ -1075,7 +1161,7 @@ describe NotificationService, services: true do end describe 'Projects' do - let(:project) { create :project } + let(:project) { create(:empty_project) } before do build_team(project) @@ -1120,7 +1206,7 @@ describe NotificationService, services: true do describe 'ProjectMember' do describe '#decline_group_invite' do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:member) { create(:user) } before(:each) do @@ -1192,6 +1278,48 @@ describe NotificationService, services: true do end end + describe 'Pipelines' do + describe '#pipeline_finished' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { create(:user) } + let(:u_member) { create(:user) } + let(:u_other) { create(:user) } + + let(:commit) { project.commit } + let(:pipeline) do + create(:ci_pipeline, :success, + project: project, + user: current_user, + ref: 'refs/heads/master', + sha: commit.id, + before_sha: '00000000') + end + + before do + project.add_master(current_user) + project.add_master(u_member) + reset_delivered_emails! + end + + context 'without custom recipients' do + it 'notifies the pipeline user' do + notification.pipeline_finished(pipeline) + + should_only_email(current_user, kind: :bcc) + end + end + + context 'with custom recipients' do + it 'notifies the custom recipients' do + users = [u_member, u_other] + notification.pipeline_finished(pipeline, users.map(&:notification_email)) + + should_only_email(*users, kind: :bcc) + end + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 74bfba44dfd..b1e10f4562e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::DestroyService, services: true do let!(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } + let!(:project) { create(:project, :repository, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:async) { false } # execute or async_execute diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index 122a7cea2a1..33b267c069c 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Projects::DownloadService, services: true do describe 'File service' do before do - @user = create :user - @project = create :project, creator_id: @user.id, namespace: @user.namespace + @user = create(:user) + @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace) end context 'for a URL that is not on whitelist' do diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 8e614211116..f8eb34f2ef4 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' describe Projects::ForkService, services: true do - describe :fork_by_user do + describe 'fork by user' do before do @from_namespace = create(:namespace) @from_user = create(:user, namespace: @from_namespace ) avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project = create(:project, + :repository, creator_id: @from_user.id, namespace: @from_namespace, star_count: 107, @@ -54,7 +55,7 @@ describe Projects::ForkService, services: true do context 'project already exists' do it "fails due to validation, not transaction failure" do - @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) + @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user) expect(@existing_project).to be_persisted @@ -100,13 +101,14 @@ describe Projects::ForkService, services: true do end end - describe :fork_to_namespace do + describe 'fork to namespace' do before do @group_owner = create(:user) @developer = create(:user) - @project = create(:project, creator_id: @group_owner.id, - star_count: 777, - description: 'Wow, such a cool project!') + @project = create(:project, :repository, + creator_id: @group_owner.id, + star_count: 777, + description: 'Wow, such a cool project!') @group = create(:group) @group.add_user(@group_owner, GroupMember::OWNER) @group.add_user(@developer, GroupMember::DEVELOPER) @@ -139,8 +141,9 @@ describe Projects::ForkService, services: true do context 'project already exists in group' do it 'fails due to validation, not transaction failure' do - existing_project = create(:project, name: @project.name, - namespace: @group) + existing_project = create(:project, :repository, + name: @project.name, + namespace: @group) to_project = fork_project(@project, @group_owner, @opts) expect(existing_project.persisted?).to be_truthy expect(to_project.errors[:name]).to eq(['has already been taken']) diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 57a5aa5cedc..eaf63457b32 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::HousekeepingService do subject { Projects::HousekeepingService.new(project) } - let(:project) { create :project } + let(:project) { create(:project, :repository) } before do project.reset_pushes_since_gc diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index ab6e8f537ba..e5917bb0b7a 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do end end + context 'with blocked import_URL' do + it 'fails with localhost' do + project.import_url = 'https://localhost:9000/vim/vim.git' + + result = described_class.new(project, user).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to end_with 'Blocked import URL.' + end + + it 'fails with port 25' do + project.import_url = "https://github.com:25/vim/vim.git" + + result = described_class.new(project, user).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to end_with 'Blocked import URL.' + end + end + def stub_github_omniauth_provider provider = OpenStruct.new( 'name' => 'github', diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5c6fbea8d0e..f8187fefc14 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::TransferService, services: true do let(:user) { create(:user) } let(:group) { create(:group) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, :repository, namespace: user.namespace) } context 'namespace -> namespace' do before do @@ -58,7 +58,7 @@ describe Projects::TransferService, services: true do before { internal_group.add_owner(user) } context 'when namespace visibility level < project visibility level' do - let(:public_project) { create(:project, :public, namespace: user.namespace) } + let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) } before { transfer_project(public_project, user, internal_group) } @@ -66,7 +66,7 @@ describe Projects::TransferService, services: true do end context 'when namespace visibility level > project visibility level' do - let(:private_project) { create(:project, :private, namespace: user.namespace) } + let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) } before { transfer_project(private_project, user, internal_group) } diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index f75fdd9e03f..fc0a17296f3 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -1,9 +1,9 @@ require "spec_helper" describe Projects::UpdatePagesService do - let(:project) { create :project } - let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha } - let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' } + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } + let(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } subject { described_class.new(project, build) } diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index caa23962519..05b18fef061 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::UpdateService, services: true do let(:user) { create(:user) } let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } describe 'update_by_user' do context 'when visibility_level is INTERNAL' do @@ -56,7 +56,7 @@ describe Projects::UpdateService, services: true do end describe 'visibility_level' do - let(:project) { create(:project, :internal) } + let(:project) { create(:empty_project, :internal) } let(:forked_project) { create(:forked_project_with_submodules, :internal) } before do diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index 150c8ccaef7..d2cefa46bfa 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Projects::UploadService, services: true do describe 'File service' do before do - @user = create :user - @project = create :project, creator_id: @user.id, namespace: @user.namespace + @user = create(:user) + @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace) end context 'for valid gif file' do diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb new file mode 100644 index 00000000000..2531607acad --- /dev/null +++ b/spec/services/search/global_service_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Search::GlobalService, services: true do + let(:user) { create(:user) } + let(:internal_user) { create(:user) } + + let!(:found_project) { create(:empty_project, :private, name: 'searchable_project') } + let!(:unfound_project) { create(:empty_project, :private, name: 'unfound_project') } + let!(:internal_project) { create(:empty_project, :internal, name: 'searchable_internal_project') } + let!(:public_project) { create(:empty_project, :public, name: 'searchable_public_project') } + + before do + found_project.add_master(user) + end + + describe '#execute' do + context 'unauthenticated' do + it 'returns public projects only' do + results = Search::GlobalService.new(nil, search: "searchable").execute + + expect(results.objects('projects')).to match_array [public_project] + end + end + + context 'authenticated' do + it 'returns public, internal and private projects' do + results = Search::GlobalService.new(user, search: "searchable").execute + + expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] + end + + it 'returns only public & internal projects' do + results = Search::GlobalService.new(internal_user, search: "searchable").execute + + expect(results.objects('projects')).to match_array [internal_project, public_project] + end + + it 'namespace name is searchable' do + results = Search::GlobalService.new(user, search: found_project.namespace.path).execute + + expect(results.objects('projects')).to match_array [found_project] + end + + context 'nested group' do + let!(:nested_group) { create(:group, :nested) } + let!(:project) { create(:empty_project, namespace: nested_group) } + + before do + project.add_master(user) + end + + it 'returns result from nested group' do + results = Search::GlobalService.new(user, search: project.path).execute + + expect(results.objects('projects')).to match_array [project] + end + + it 'returns result from descendants when search inside group' do + results = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent).execute + + expect(results.objects('projects')).to match_array [project] + end + end + end + end +end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index bed1031e40a..2112f1cf9ea 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -1,65 +1,286 @@ require 'spec_helper' -describe 'Search::GlobalService', services: true do +describe SearchService, services: true do let(:user) { create(:user) } - let(:public_user) { create(:user) } - let(:internal_user) { create(:user) } - let!(:found_project) { create(:empty_project, :private, name: 'searchable_project') } - let!(:unfound_project) { create(:empty_project, :private, name: 'unfound_project') } - let!(:internal_project) { create(:empty_project, :internal, name: 'searchable_internal_project') } - let!(:public_project) { create(:empty_project, :public, name: 'searchable_public_project') } + let(:accessible_group) { create(:group, :private) } + let(:inaccessible_group) { create(:group, :private) } + let!(:group_member) { create(:group_member, group: accessible_group, user: user) } + + let!(:accessible_project) { create(:empty_project, :private, name: 'accessible_project') } + let!(:inaccessible_project) { create(:empty_project, :private, name: 'inaccessible_project') } + let(:note) { create(:note_on_issue, project: accessible_project) } + + let(:snippet) { create(:snippet, author: user) } + let(:group_project) { create(:empty_project, group: accessible_group, name: 'group_project') } + let(:public_project) { create(:empty_project, :public, name: 'public_project') } before do - found_project.team << [user, :master] + accessible_project.add_master(user) + end + + describe '#project' do + context 'when the project is accessible' do + it 'returns the project' do + project = SearchService.new(user, project_id: accessible_project.id).project + + expect(project).to eq accessible_project + end + end + + context 'when the project is not accessible' do + it 'returns nil' do + project = SearchService.new(user, project_id: inaccessible_project.id).project + + expect(project).to be_nil + end + end + + context 'when there is no project_id' do + it 'returns nil' do + project = SearchService.new(user).project + + expect(project).to be_nil + end + end end - describe '#execute' do - context 'unauthenticated' do - it 'returns public projects only' do - context = Search::GlobalService.new(nil, search: "searchable") - results = context.execute - expect(results.objects('projects')).to match_array [public_project] + describe '#group' do + context 'when the group is accessible' do + it 'returns the group' do + group = SearchService.new(user, group_id: accessible_group.id).group + + expect(group).to eq accessible_group end end - context 'authenticated' do - it 'returns public, internal and private projects' do - context = Search::GlobalService.new(user, search: "searchable") - results = context.execute - expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] + context 'when the group is not accessible' do + it 'returns nil' do + group = SearchService.new(user, group_id: inaccessible_group.id).group + + expect(group).to be_nil end + end + + context 'when there is no group_id' do + it 'returns nil' do + group = SearchService.new(user).group - it 'returns only public & internal projects' do - context = Search::GlobalService.new(internal_user, search: "searchable") - results = context.execute - expect(results.objects('projects')).to match_array [internal_project, public_project] + expect(group).to be_nil end + end + end + + describe '#show_snippets?' do + context 'when :snippets is \'true\'' do + it 'returns true' do + show_snippets = SearchService.new(user, snippets: 'true').show_snippets? - it 'namespace name is searchable' do - context = Search::GlobalService.new(user, search: found_project.namespace.path) - results = context.execute - expect(results.objects('projects')).to match_array [found_project] + expect(show_snippets).to be_truthy end + end - context 'nested group' do - let!(:nested_group) { create(:group, :nested) } - let!(:project) { create(:project, namespace: nested_group) } + context 'when :snippets is not \'true\'' do + it 'returns false' do + show_snippets = SearchService.new(user, snippets: 'tru').show_snippets? + + expect(show_snippets).to be_falsey + end + end - before { project.add_master(user) } + context 'when :snippets is missing' do + it 'returns false' do + show_snippets = SearchService.new(user).show_snippets? - it 'returns result from nested group' do - context = Search::GlobalService.new(user, search: project.path) - results = context.execute - expect(results.objects('projects')).to match_array [project] + expect(show_snippets).to be_falsey + end + end + end + + describe '#scope' do + context 'with accessible project_id' do + context 'and allowed scope' do + it 'returns the specified scope' do + scope = SearchService.new(user, project_id: accessible_project.id, scope: 'notes').scope + + expect(scope).to eq 'notes' end + end + + context 'and disallowed scope' do + it 'returns the default scope' do + scope = SearchService.new(user, project_id: accessible_project.id, scope: 'projects').scope - it 'returns result from descendants when search inside group' do - context = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent) - results = context.execute - expect(results.objects('projects')).to match_array [project] + expect(scope).to eq 'blobs' end end + + context 'and no scope' do + it 'returns the default scope' do + scope = SearchService.new(user, project_id: accessible_project.id).scope + + expect(scope).to eq 'blobs' + end + end + end + + context 'with \'true\' snippets' do + context 'and allowed scope' do + it 'returns the specified scope' do + scope = SearchService.new(user, snippets: 'true', scope: 'snippet_titles').scope + + expect(scope).to eq 'snippet_titles' + end + end + + context 'and disallowed scope' do + it 'returns the default scope' do + scope = SearchService.new(user, snippets: 'true', scope: 'projects').scope + + expect(scope).to eq 'snippet_blobs' + end + end + + context 'and no scope' do + it 'returns the default scope' do + scope = SearchService.new(user, snippets: 'true').scope + + expect(scope).to eq 'snippet_blobs' + end + end + end + + context 'with no project_id, no snippets' do + context 'and allowed scope' do + it 'returns the specified scope' do + scope = SearchService.new(user, scope: 'issues').scope + + expect(scope).to eq 'issues' + end + end + + context 'and disallowed scope' do + it 'returns the default scope' do + scope = SearchService.new(user, scope: 'blobs').scope + + expect(scope).to eq 'projects' + end + end + + context 'and no scope' do + it 'returns the default scope' do + scope = SearchService.new(user).scope + + expect(scope).to eq 'projects' + end + end + end + end + + describe '#search_results' do + context 'with accessible project_id' do + it 'returns an instance of Gitlab::ProjectSearchResults' do + search_results = SearchService.new( + user, + project_id: accessible_project.id, + scope: 'notes', + search: note.note).search_results + + expect(search_results).to be_a Gitlab::ProjectSearchResults + end + end + + context 'with accessible project_id and \'true\' snippets' do + it 'returns an instance of Gitlab::ProjectSearchResults' do + search_results = SearchService.new( + user, + project_id: accessible_project.id, + snippets: 'true', + scope: 'notes', + search: note.note).search_results + + expect(search_results).to be_a Gitlab::ProjectSearchResults + end + end + + context 'with \'true\' snippets' do + it 'returns an instance of Gitlab::SnippetSearchResults' do + search_results = SearchService.new( + user, + snippets: 'true', + search: snippet.content).search_results + + expect(search_results).to be_a Gitlab::SnippetSearchResults + end + end + + context 'with no project_id and no snippets' do + it 'returns an instance of Gitlab::SearchResults' do + search_results = SearchService.new( + user, + search: public_project.name).search_results + + expect(search_results).to be_a Gitlab::SearchResults + end + end + end + + describe '#search_objects' do + context 'with accessible project_id' do + it 'returns objects in the project' do + search_objects = SearchService.new( + user, + project_id: accessible_project.id, + scope: 'notes', + search: note.note).search_objects + + expect(search_objects.first).to eq note + end + end + + context 'with accessible project_id and \'true\' snippets' do + it 'returns objects in the project' do + search_objects = SearchService.new( + user, + project_id: accessible_project.id, + snippets: 'true', + scope: 'notes', + search: note.note).search_objects + + expect(search_objects.first).to eq note + end + end + + context 'with \'true\' snippets' do + it 'returns objects in snippets' do + search_objects = SearchService.new( + user, + snippets: 'true', + search: snippet.content).search_objects + + expect(search_objects.first).to eq snippet + end + end + + context 'with accessible group_id' do + it 'returns objects in the group' do + search_objects = SearchService.new( + user, + group_id: accessible_group.id, + search: group_project.name).search_objects + + expect(search_objects.first).to eq group_project + end + end + + context 'with no project_id, group_id or snippets' do + it 'returns objects in global' do + search_objects = SearchService.new( + user, + search: public_project.name).search_objects + + expect(search_objects.first).to eq public_project + end end end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 52e8678cb9d..a63281f0eab 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe SlashCommands::InterpretService, services: true do - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:developer) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } @@ -260,6 +260,8 @@ describe SlashCommands::InterpretService, services: true do end shared_examples 'merge command' do + let(:project) { create(:project, :repository) } + it 'runs merge command if content contains /merge' do _, updates = service.execute(content, issuable) @@ -322,6 +324,7 @@ describe SlashCommands::InterpretService, services: true do end context 'when sha is missing' do + let(:project) { create(:project, :repository) } let(:service) { described_class.new(project, developer, {}) } it 'precheck passes and returns merge command' do @@ -694,7 +697,7 @@ describe SlashCommands::InterpretService, services: true do end context '/target_branch command' do - let(:non_empty_project) { create(:project) } + let(:non_empty_project) { create(:project, :repository) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } let(:service) { described_class.new(non_empty_project, developer)} diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb index 4ce3b95aa87..74cba8c014b 100644 --- a/spec/services/spam_service_spec.rb +++ b/spec/services/spam_service_spec.rb @@ -15,46 +15,71 @@ describe SpamService, services: true do end context 'when recaptcha was not verified' do - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } let(:request) { double(:request, env: {}) } - context 'when indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) } + context 'when spammable attributes have not changed' do + before do + issue.closed_at = Time.zone.now - it 'doesnt check as spam when request is missing' do - check_spam(issue, nil, false) - - expect(issue.spam).to be_falsey + allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) end - it 'checks as spam' do - check_spam(issue, request, false) - - expect(issue.spam).to be_truthy + it 'returns false' do + expect(check_spam(issue, request, false)).to be_falsey end - it 'creates a spam log' do + it 'does not create a spam log' do expect { check_spam(issue, request, false) } - .to change { SpamLog.count }.from(0).to(1) + .not_to change { SpamLog.count } end + end - it 'doesnt yield block' do - expect(check_spam(issue, request, false)) - .to eql(SpamLog.last) + context 'when spammable attributes have changed' do + before do + issue.description = 'SPAM!' end - end - context 'when not indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + context 'when indicated as spam by akismet' do + before do + allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) + end - it 'returns false' do - expect(check_spam(issue, request, false)).to be_falsey + it 'doesnt check as spam when request is missing' do + check_spam(issue, nil, false) + + expect(issue.spam).to be_falsey + end + + it 'checks as spam' do + check_spam(issue, request, false) + + expect(issue.spam).to be_truthy + end + + it 'creates a spam log' do + expect { check_spam(issue, request, false) } + .to change { SpamLog.count }.from(0).to(1) + end + + it 'doesnt yield block' do + expect(check_spam(issue, request, false)) + .to eql(SpamLog.last) + end end - it 'does not create a spam log' do - expect { check_spam(issue, request, false) } - .not_to change { SpamLog.count } + context 'when not indicated as spam by akismet' do + before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + + it 'returns false' do + expect(check_spam(issue, request, false)).to be_falsey + end + + it 'does not create a spam log' do + expect { check_spam(issue, request, false) } + .not_to change { SpamLog.count } + end end end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index db9f1231682..667059f230c 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' describe SystemHooksService, services: true do - let(:user) { create :user } - let(:project) { create :project } - let(:project_member) { create :project_member } - let(:key) { create(:key, user: user) } - let(:group) { create(:group) } - let(:group_member) { create(:group_member) } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:project_member) { create(:project_member) } + let(:key) { create(:key, user: user) } + let(:deploy_key) { create(:key) } + let(:group) { create(:group) } + let(:group_member) { create(:group_member) } context 'event data' do it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } @@ -18,6 +19,8 @@ describe SystemHooksService, services: true do it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } + it { expect(event_data(deploy_key, :create)).to include(:key, :id) } + it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) } it do project.old_path_with_namespace = 'renamed_from_path' diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 36a17a3bf2e..90cde705b85 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -3,17 +3,20 @@ require 'spec_helper' describe SystemNoteService, services: true do include Gitlab::Routing.url_helpers - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:author) { create(:user) } let(:noteable) { create(:issue, project: project) } shared_examples_for 'a system note' do + let(:expected_noteable) { noteable } + let(:commit_count) { nil } + it 'is valid' do expect(subject).to be_valid end it 'sets the noteable model' do - expect(subject.noteable).to eq noteable + expect(subject.noteable).to eq expected_noteable end it 'sets the project' do @@ -27,17 +30,34 @@ describe SystemNoteService, services: true do it 'is a system note' do expect(subject).to be_system end + + context 'metadata' do + it 'creates a new system note metadata record' do + expect { subject }.to change{ SystemNoteMetadata.count }.from(0).to(1) + end + + it 'creates a record correctly' do + metadata = subject.system_note_metadata + + expect(metadata.commit_count).to eq(commit_count) + expect(metadata.action).to eq(action) + end + end end describe '.add_commits' do subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) } + let(:project) { create(:project, :repository) } let(:noteable) { create(:merge_request, source_project: project) } let(:new_commits) { noteable.commits } let(:old_commits) { [] } let(:oldrev) { nil } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:commit_count) { new_commits.size } + let(:action) { 'commit' } + end describe 'note body' do let(:note_lines) { subject.note.split("\n").reject(&:blank?) } @@ -116,7 +136,9 @@ describe SystemNoteService, services: true do let(:assignee) { create(:user) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'assignee' } + end context 'when assignee added' do it 'sets the note text' do @@ -140,7 +162,9 @@ describe SystemNoteService, services: true do let(:added) { [] } let(:removed) { [] } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'label' } + end context 'with added labels' do let(:added) { labels } @@ -175,7 +199,9 @@ describe SystemNoteService, services: true do let(:milestone) { create(:milestone, project: project) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'milestone' } + end context 'when milestone added' do it 'sets the note text' do @@ -198,7 +224,9 @@ describe SystemNoteService, services: true do let(:status) { 'new_status' } let(:source) { nil } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'status' } + end context 'with a source' do let(:source) { double('commit', gfm_reference: 'commit 123456') } @@ -216,6 +244,7 @@ describe SystemNoteService, services: true do end describe '.merge_when_pipeline_succeeds' do + let(:project) { create(:project, :repository) } let(:pipeline) { build(:ci_pipeline_without_jobs )} let(:noteable) do create(:merge_request, source_project: project, target_project: project) @@ -223,21 +252,26 @@ describe SystemNoteService, services: true do subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'merge' } + end it "posts the 'merge when pipeline succeeds' system note" do - expect(subject.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/ + expect(subject.note).to match(/enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/) end end describe '.cancel_merge_when_pipeline_succeeds' do + let(:project) { create(:project, :repository) } let(:noteable) do create(:merge_request, source_project: project, target_project: project) end subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'merge' } + end it "posts the 'merge when pipeline succeeds' system note" do expect(subject.note).to eq "canceled the automatic merge" @@ -250,7 +284,9 @@ describe SystemNoteService, services: true do subject { described_class.change_title(noteable, project, author, 'Old title') } context 'when noteable responds to `title`' do - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'title' } + end it 'sets the note text' do expect(subject.note). @@ -263,7 +299,9 @@ describe SystemNoteService, services: true do subject { described_class.change_issue_confidentiality(noteable, project, author) } context 'when noteable responds to `confidential`' do - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'confidentiality' } + end it 'sets the note text' do expect(subject.note).to eq 'made the issue visible to everyone' @@ -273,10 +311,14 @@ describe SystemNoteService, services: true do describe '.change_branch' do subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } + + let(:project) { create(:project, :repository) } let(:old_branch) { 'old_branch'} let(:new_branch) { 'new_branch'} - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'branch' } + end context 'when target branch name changed' do it 'sets the note text' do @@ -288,7 +330,11 @@ describe SystemNoteService, services: true do describe '.change_branch_presence' do subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) } - it_behaves_like 'a system note' + let(:project) { create(:project, :repository) } + + it_behaves_like 'a system note' do + let(:action) { 'branch' } + end context 'when source branch deleted' do it 'sets the note text' do @@ -300,11 +346,15 @@ describe SystemNoteService, services: true do describe '.new_issue_branch' do subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") } - it_behaves_like 'a system note' + let(:project) { create(:project, :repository) } + + it_behaves_like 'a system note' do + let(:action) { 'branch' } + end context 'when a branch is created from the new branch button' do it 'sets the note text' do - expect(subject.note).to match /\Acreated branch [`1-mepmep`]/ + expect(subject.note).to start_with("created branch [`1-mepmep`]") end end end @@ -314,7 +364,9 @@ describe SystemNoteService, services: true do let(:mentioner) { create(:issue, project: project) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'cross_reference' } + end context 'when cross-reference disallowed' do before do @@ -324,6 +376,10 @@ describe SystemNoteService, services: true do it 'returns nil' do expect(subject).to be_nil end + + it 'does not create a system note metadata record' do + expect { subject }.not_to change{ SystemNoteMetadata.count } + end end context 'when cross-reference allowed' do @@ -331,9 +387,13 @@ describe SystemNoteService, services: true do expect(described_class).to receive(:cross_reference_disallowed?).and_return(false) end + it_behaves_like 'a system note' do + let(:action) { 'cross_reference' } + end + describe 'note_body' do context 'cross-project' do - let(:project2) { create(:project) } + let(:project2) { create(:project, :repository) } let(:mentioner) { create(:issue, project: project2) } context 'from Commit' do @@ -353,6 +413,7 @@ describe SystemNoteService, services: true do context 'within the same project' do context 'from Commit' do + let(:project) { create(:project, :repository) } let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do @@ -394,6 +455,7 @@ describe SystemNoteService, services: true do end context 'when mentioner is a MergeRequest' do + let(:project) { create(:project, :repository) } let(:mentioner) { create(:merge_request, :simple, source_project: project) } let(:noteable) { project.commit } @@ -421,6 +483,7 @@ describe SystemNoteService, services: true do end describe '.cross_reference_exists?' do + let(:project) { create(:project, :repository) } let(:commit0) { project.commit } let(:commit1) { project.commit('HEAD~2') } @@ -513,7 +576,7 @@ describe SystemNoteService, services: true do end describe '.noteable_moved' do - let(:new_project) { create(:project) } + let(:new_project) { create(:empty_project) } let(:new_noteable) { create(:issue, project: new_project) } subject do @@ -540,9 +603,12 @@ describe SystemNoteService, services: true do let(:direction) { :to } it_behaves_like 'cross project mentionable' + it_behaves_like 'a system note' do + let(:action) { 'moved' } + end it 'notifies about noteable being moved to' do - expect(subject.note).to match /moved to/ + expect(subject.note).to match('moved to') end end @@ -550,9 +616,12 @@ describe SystemNoteService, services: true do let(:direction) { :from } it_behaves_like 'cross project mentionable' + it_behaves_like 'a system note' do + let(:action) { 'moved' } + end it 'notifies about noteable being moved from' do - expect(subject.note).to match /moved from/ + expect(subject.note).to match('moved from') end end @@ -574,13 +643,13 @@ describe SystemNoteService, services: true do end end - include JiraServiceHelper - describe 'JIRA integration' do + include JiraServiceHelper + let(:project) { create(:jira_project) } let(:author) { create(:user) } let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) } + let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) } let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} let(:jira_tracker) { project.jira_service } let(:commit) { project.commit } @@ -720,33 +789,34 @@ describe SystemNoteService, services: true do let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } let(:issue) { create(:issue, project: project) } - let(:user) { create(:user) } def reloaded_merge_request MergeRequest.find(merge_request.id) end - before do - project.team << [user, :developer] + subject { described_class.discussion_continued_in_issue(discussion, project, author, issue) } + + it_behaves_like 'a system note' do + let(:expected_noteable) { discussion.first_note.noteable } + let(:action) { 'discussion' } end it 'creates a new note in the discussion' do # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. - expect { SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) }. - to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) end it 'mentions the created issue in the system note' do - note = SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) - - expect(note.note).to include(issue.to_reference) + expect(subject.note).to include(issue.to_reference) end end describe '.change_time_estimate' do subject { described_class.change_time_estimate(noteable, project, author) } - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'time_tracking' } + end context 'with a time estimate' do it 'sets the note text' do @@ -776,7 +846,9 @@ describe SystemNoteService, services: true do described_class.change_time_spent(noteable, project, author) end - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'time_tracking' } + end context 'when time was added' do it 'sets the note text' do @@ -808,7 +880,36 @@ describe SystemNoteService, services: true do end end + describe '.remove_merge_request_wip' do + let(:noteable) { create(:issue, project: project, title: 'WIP: Lorem ipsum') } + + subject { described_class.remove_merge_request_wip(noteable, project, author) } + + it_behaves_like 'a system note' do + let(:action) { 'title' } + end + + it 'sets the note text' do + expect(subject.note).to eq 'unmarked as a **Work In Progress**' + end + end + + describe '.add_merge_request_wip' do + let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } + + subject { described_class.add_merge_request_wip(noteable, project, author) } + + it_behaves_like 'a system note' do + let(:action) { 'title' } + end + + it 'sets the note text' do + expect(subject.note).to eq 'marked as a **Work In Progress**' + end + end + describe '.add_merge_request_wip_from_commit' do + let(:project) { create(:project, :repository) } let(:noteable) do create(:merge_request, source_project: project, target_project: project) end @@ -822,7 +923,9 @@ describe SystemNoteService, services: true do ) end - it_behaves_like 'a system note' + it_behaves_like 'a system note' do + let(:action) { 'title' } + end it "posts the 'marked as a Work In Progress from commit' system note" do expect(subject.note).to match( @@ -830,4 +933,33 @@ describe SystemNoteService, services: true do ) end end + + describe '.change_task_status' do + let(:noteable) { create(:issue, project: project) } + let(:task) { double(:task, complete?: true, source: 'task') } + + subject { described_class.change_task_status(noteable, project, author, task) } + + it_behaves_like 'a system note' do + let(:action) { 'task' } + end + + it "posts the 'marked as a Work In Progress from commit' system note" do + expect(subject.note).to eq("marked the task **task** as completed") + end + end + + describe '.resolve_all_discussions' do + let(:noteable) { create(:merge_request, source_project: project, target_project: project) } + + subject { described_class.resolve_all_discussions(noteable, project, author) } + + it_behaves_like 'a system note' do + let(:action) { 'discussion' } + end + + it 'sets the note text' do + expect(subject.note).to eq 'resolved all discussions' + end + end end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index 5478b8c9ec0..b9121b1de49 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Tags::CreateService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index a388c93379a..28396fc3658 100644 --- a/spec/services/tags/destroy_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Tags::DestroyService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index 4f6dd8c6d3f..f99fd8434c2 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe TestHookService, services: true do - let(:user) { create :user } - let(:project) { create :project } - let(:hook) { create :project_hook, project: project } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:hook) { create(:project_hook, project: project) } describe '#execute' do it "executes successfully" do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 3645b73b039..89b3b6aad10 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -8,10 +8,12 @@ describe TodoService, services: true do let(:guest) { create(:user) } let(:admin) { create(:admin) } let(:john_doe) { create(:user) } - let(:project) { create(:project) } - 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(:skipped) { create(:user) } + let(:skip_users) { [skipped] } + let(:project) { create(:empty_project) } + let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } + let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } + let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') } let(:service) { described_class.new } before do @@ -19,6 +21,7 @@ describe TodoService, services: true do project.team << [author, :developer] project.team << [member, :developer] project.team << [john_doe, :developer] + project.team << [skipped, :developer] end describe 'Issues' do @@ -99,9 +102,9 @@ describe TodoService, services: true do end context 'when a private group is mentioned' do - let(:group) { create :group, :private } - let(:project) { create :project, :private, group: group } - let(:issue) { create :issue, author: author, project: project, description: group.to_reference } + let(:group) { create(:group, :private) } + let(:project) { create(:empty_project, :private, group: group) } + let(:issue) { create(:issue, author: author, project: project, description: group.to_reference) } before do group.add_owner(author) @@ -119,46 +122,61 @@ describe TodoService, services: true do end describe '#update_issue' do - it 'creates a todo for each valid mentioned user' do - service.update_issue(issue, author) + it 'creates a todo for each valid mentioned user not included in skip_users' do + service.update_issue(issue, author, skip_users) should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: skipped, target: issue, action: Todo::MENTIONED) end - it 'creates a todo for each valid user based on the type of mention' do + it 'creates a todo for each valid user not included in skip_users based on the type of mention' do issue.update(description: directly_addressed_and_mentioned) - service.update_issue(issue, author) + service.update_issue(issue, author, skip_users) 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) + should_not_create_todo(user: skipped, target: issue) end - it 'creates a directly addressed todo for each valid addressed user' do - service.update_issue(addressed_issue, author) + it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do + service.update_issue(addressed_issue, author, skip_users) 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) + should_not_create_todo(user: skipped, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) end - it 'does not create a todo if user was already mentioned' do + it 'does not create a todo if user was already mentioned and todo is pending' do create(:todo, :mentioned, user: member, project: project, target: issue, author: author) - expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) + expect { service.update_issue(issue, author, skip_users) }.not_to change(member.todos, :count) + end + + it 'does not create a todo if user was already mentioned and todo is done' do + create(:todo, :mentioned, :done, user: skipped, project: project, target: issue, author: author) + + expect { service.update_issue(issue, author, skip_users) }.not_to change(skipped.todos, :count) end - it 'does not create a directly addressed todo if user was already mentioned or addressed' do + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' 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) + expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(member.todos, :count) + end + + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do + create(:todo, :directly_addressed, :done, user: skipped, project: project, target: addressed_issue, author: author) + + expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(skipped.todos, :count) end it 'does not create todo if user can not see the issue when issue is confidential' do @@ -422,22 +440,26 @@ describe TodoService, services: true do 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) + context 'on commit' do + let(:project) { create(:project, :repository) } - should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - 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 todo for each valid mentioned user when leaving a note on commit' do + service.new_note(note_on_commit, john_doe) + + should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + 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) + 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) + 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 end it 'does not create todo when leaving a note on snippet' do @@ -517,47 +539,62 @@ describe TodoService, services: true do end describe '#update_merge_request' do - it 'creates a todo for each valid mentioned user' do - service.update_merge_request(mr_assigned, author) + it 'creates a todo for each valid mentioned user not included in skip_users' do + service.update_merge_request(mr_assigned, author, skip_users) should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: skipped, target: mr_assigned, action: Todo::MENTIONED) end - it 'creates a todo for each valid user based on the type of mention' do + it 'creates a todo for each valid user not included in skip_users based on the type of mention' do mr_assigned.update(description: directly_addressed_and_mentioned) - service.update_merge_request(mr_assigned, author) + service.update_merge_request(mr_assigned, author, skip_users) should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: skipped, target: mr_assigned) end - it 'creates a directly addressed todo for each valid addressed user' do - service.update_merge_request(addressed_mr_assigned, author) + it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do + service.update_merge_request(addressed_mr_assigned, author, skip_users) 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) + should_not_create_todo(user: skipped, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) end - it 'does not create a todo if user was already mentioned' do + it 'does not create a todo if user was already mentioned and todo is pending' 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 + it 'does not create a todo if user was already mentioned and todo is done' do + create(:todo, :mentioned, :done, user: skipped, project: project, target: mr_assigned, author: author) + + expect { service.update_merge_request(mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count) + end + + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' 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 + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do + create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author) + + expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.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}") @@ -720,6 +757,7 @@ describe TodoService, services: true do end describe '#new_note' do + let(:project) { create(:project, :repository) } 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!") } @@ -752,6 +790,69 @@ describe TodoService, services: true do end end + describe '#update_note' do + let(:noteable) { create(:issue, project: project) } + let(:note) { create(:note, project: project, note: mentions, noteable: noteable) } + let(:addressed_note) { create(:note, project: project, note: "#{directly_addressed}", noteable: noteable) } + + it 'creates a todo for each valid mentioned user not included in skip_users' do + service.update_note(note, author, skip_users) + + should_create_todo(user: member, target: noteable, action: Todo::MENTIONED) + should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: noteable, action: Todo::MENTIONED) + should_create_todo(user: author, target: noteable, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: noteable, action: Todo::MENTIONED) + should_not_create_todo(user: skipped, target: noteable, action: Todo::MENTIONED) + end + + it 'creates a todo for each valid user not included in skip_users based on the type of mention' do + note.update(note: directly_addressed_and_mentioned) + + service.update_note(note, author, skip_users) + + should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED) + should_create_todo(user: admin, target: noteable, action: Todo::MENTIONED) + should_not_create_todo(user: skipped, target: noteable) + end + + it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do + service.update_note(addressed_note, author, skip_users) + + should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: skipped, target: noteable, action: Todo::DIRECTLY_ADDRESSED) + end + + it 'does not create a todo if user was already mentioned and todo is pending' do + create(:todo, :mentioned, user: member, project: project, target: noteable, author: author) + + expect { service.update_note(note, author, skip_users) }.not_to change(member.todos, :count) + end + + it 'does not create a todo if user was already mentioned and todo is done' do + create(:todo, :mentioned, :done, user: skipped, project: project, target: noteable, author: author) + + expect { service.update_note(note, author, skip_users) }.not_to change(skipped.todos, :count) + end + + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do + create(:todo, :directly_addressed, user: member, project: project, target: noteable, author: author) + + expect { service.update_note(addressed_note, author, skip_users) }.not_to change(member.todos, :count) + end + + it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do + create(:todo, :directly_addressed, :done, user: skipped, project: project, target: noteable, author: author) + + expect { service.update_note(addressed_note, author, skip_users) }.not_to change(skipped.todos, :count) + end + end + it 'updates cached counts when a todo is created' do issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions) diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb index bba211089a8..69ed8de9c31 100644 --- a/spec/services/update_release_service_spec.rb +++ b/spec/services/update_release_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe UpdateReleaseService, services: true do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb new file mode 100644 index 00000000000..66f68650f81 --- /dev/null +++ b/spec/services/users/create_service_spec.rb @@ -0,0 +1,199 @@ +require 'spec_helper' + +describe Users::CreateService, services: true do + describe '#build' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + context 'with an admin user' do + let(:admin_user) { create(:admin) } + let(:service) { described_class.new(admin_user, params) } + + it 'returns a valid user' do + expect(service.build).to be_valid + end + end + + context 'with non admin user' do + let(:user) { create(:user) } + let(:service) { described_class.new(user, params) } + + it 'raises AccessDeniedError exception' do + expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'with nil user' do + let(:service) { described_class.new(nil, params) } + + it 'returns a valid user' do + expect(service.build).to be_valid + end + end + end + + describe '#execute' do + let(:admin_user) { create(:admin) } + + context 'with an admin user' do + let(:service) { described_class.new(admin_user, params) } + + context 'when required parameters are provided' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + it 'returns a persisted user' do + expect(service.execute).to be_persisted + end + + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: admin_user.id + ) + end + + context 'when the current_user is not persisted' do + let(:admin_user) { build(:admin) } + + it 'persists the given attributes and sets created_by_id to nil' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: nil + ) + end + end + + it 'user is not confirmed if skip_confirmation param is not present' do + expect(service.execute).not_to be_confirmed + end + + it 'logs the user creation' do + expect(service).to receive(:log_info).with("User \"John Doe\" (jd@example.com) was created") + + service.execute + end + + it 'executes system hooks ' do + system_hook_service = spy(:system_hook_service) + + expect(service).to receive(:system_hook_service).and_return(system_hook_service) + + user = service.execute + + expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create) + end + + it 'does not send a notification email' do + notification_service = spy(:notification_service) + + expect(service).not_to receive(:notification_service) + + service.execute + + expect(notification_service).not_to have_received(:new_user) + end + end + + context 'when force_random_password parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true } + end + + it 'generates random password' do + user = service.execute + + expect(user.password).not_to eq 'mydummypass' + expect(user.password).to be_present + end + end + + context 'when skip_confirmation parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + + context 'when reset_password parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true } + end + + it 'resets password even if a password parameter is given' do + expect(service.execute).to be_recently_sent_password_reset + end + + it 'sends a notification email' do + notification_service = spy(:notification_service) + + expect(service).to receive(:notification_service).and_return(notification_service) + + user = service.execute + + expect(notification_service).to have_received(:new_user).with(user, an_instance_of(String)) + end + end + end + + context 'with nil user' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + end + let(:service) { described_class.new(nil, params) } + + context 'when "send_user_confirmation_email" application setting is true' do + before do + current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true) + allow(service).to receive(:current_application_settings).and_return(current_application_settings) + end + + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + + context 'when "send_user_confirmation_email" application setting is false' do + before do + current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true) + allow(service).to receive(:current_application_settings).and_return(current_application_settings) + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: nil, + admin: false + ) + end + end + end + end +end diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index 922e82445d0..66c61b7f8ff 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -5,7 +5,7 @@ describe Users::DestroyService, services: true do let!(:user) { create(:user) } let!(:admin) { create(:admin) } let!(:namespace) { create(:namespace, owner: user) } - let!(:project) { create(:project, namespace: namespace) } + let!(:project) { create(:empty_project, namespace: namespace) } let(:service) { described_class.new(admin) } context 'no options are given' do @@ -17,15 +17,30 @@ describe Users::DestroyService, services: true do expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end - it 'will delete the project in the near future' do - expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once + it 'will delete the project' do + expect_any_instance_of(Projects::DestroyService).to receive(:execute).once service.execute(user) end end + context 'projects in pending_delete' do + before do + project.pending_delete = true + project.save + end + + it 'destroys a project in pending_delete' do + expect_any_instance_of(Projects::DestroyService).to receive(:execute).once + + service.execute(user) + + expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context "a deleted user's issues" do - let(:project) { create :project } + let(:project) { create(:project) } before do project.add_developer(user) diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 08733d6dcf1..b19374ef1a2 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -152,7 +152,7 @@ describe Users::RefreshAuthorizedProjectsService do context 'projects of groups the user is a member of' do let(:group) { create(:group) } - let!(:other_project) { create(:project, group: group) } + let!(:other_project) { create(:empty_project, group: group) } before do group.add_owner(user) @@ -166,7 +166,7 @@ describe Users::RefreshAuthorizedProjectsService do context 'projects of subgroups of groups the user is a member of' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } - let!(:other_project) { create(:project, group: nested_group) } + let!(:other_project) { create(:empty_project, group: nested_group) } before do group.add_master(user) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ceb3209331f..4eb5b150af5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,7 +9,8 @@ require 'rspec/rails' require 'shoulda/matchers' require 'rspec/retry' -if ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING'] +if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) && + (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master') require 'rspec_profiling/rspec' end @@ -35,7 +36,8 @@ RSpec.configure do |config| config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature - config.include WaitForAjax, type: :feature + config.include WaitForRequests, :js + config.include WaitForAjax, :js config.include StubConfiguration config.include EmailHelpers, type: :mailer config.include TestEnv diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index aa14709bc9c..b8ca8f22a3d 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,10 +1,11 @@ +# rubocop:disable Style/GlobalVars require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' require 'capybara-screenshot/rspec' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| @@ -26,7 +27,10 @@ Capybara.ignore_hidden_elements = true Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| - config.before(:suite) do - TestEnv.warm_asset_cache + config.before(:context, :js) do + next if $capybara_server_already_started + + TestEnv.eager_load_driver_server + $capybara_server_already_started = true end end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index d0fd2d52004..51f1015f43c 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -228,5 +228,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do post :create, { new_name: test_name, format: :js } end end + + context 'user has chosen a nested namespace and name for the project' do + let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } + let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) } + let(:test_name) { 'test_name' } + + it 'takes the selected namespace and name' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js } + end + end end end diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index a3724b801b3..76411065265 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -3,7 +3,7 @@ shared_context 'gitlab email notification' do let(:gitlab_sender) { Gitlab.config.gitlab.email_from } let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } let(:recipient) { create(:user, email: 'recipient@example.com') } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:new_user_address) { 'newguy@example.com' } before do @@ -27,24 +27,14 @@ shared_examples 'a multiple recipients email' do end shared_examples 'an email sent from GitLab' do - it 'is sent from GitLab' do + it 'has the characteristics of an email sent from GitLab' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(gitlab_sender_display_name) - expect(sender.address).to eq(gitlab_sender) - end - - it 'has a Reply-To address' do reply_to = subject.header[:reply_to].addresses - expect(reply_to).to eq([gitlab_sender_reply_to]) - end - - context 'when custom suffix for email subject is set' do - before do - stub_config_setting(email_subject_suffix: 'A Nice Suffix') - end - it 'ends the subject with the suffix' do - is_expected.to have_subject /\ \| A Nice Suffix$/ + aggregate_failures do + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + expect(reply_to).to eq([gitlab_sender_reply_to]) end end end @@ -56,43 +46,40 @@ shared_examples 'an email that contains a header with author username' do end shared_examples 'an email with X-GitLab headers containing project details' do - it 'has X-GitLab-Project* headers' do - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ - is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ + it 'has X-GitLab-Project headers' do + aggregate_failures do + is_expected.to have_header('X-GitLab-Project', /#{project.name}/) + is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/) + is_expected.to have_header('X-GitLab-Project-Path', /#{project.path_with_namespace}/) + end end end shared_examples 'a new thread email with reply-by-email enabled' do - let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ } - - it 'has a Message-ID header' do - is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" - end + it 'has the characteristics of a threaded email' do + host = Gitlab.config.gitlab.host + route_key = "#{model.class.model_name.singular_route_key}_#{model.id}" - it 'has a References header' do - is_expected.to have_header 'References', regex + aggregate_failures do + is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>") + is_expected.to have_header('References', /\A<reply\-.*@#{host}>\Z/ ) + end end end shared_examples 'a thread answer email with reply-by-email enabled' do include_examples 'an email with X-GitLab headers containing project details' - let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ } - - it 'has a Message-ID header' do - is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/ - end - - it 'has a In-Reply-To header' do - is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" - end - it 'has a References header' do - is_expected.to have_header 'References', regex - end + it 'has the characteristics of a threaded reply' do + host = Gitlab.config.gitlab.host + route_key = "#{model.class.model_name.singular_route_key}_#{model.id}" - it 'has a subject that begins with Re: ' do - is_expected.to have_subject /^Re: / + aggregate_failures do + is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/) + is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>") + is_expected.to have_header('References', /\A<#{route_key}@#{host}> <reply\-.*@#{host}>\Z/ ) + is_expected.to have_subject(/^Re: /) + end end end @@ -136,80 +123,77 @@ shared_examples 'an answer to an existing thread with reply-by-email enabled' do end end -shared_examples 'a new user email' do - it 'is sent to the new user' do - is_expected.to deliver_to new_user_address - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{new_user_address}/ - end -end - shared_examples 'it should have Gmail Actions links' do - it { is_expected.to have_body_text '<script type="application/ld+json">' } - it { is_expected.to have_body_text /ViewAction/ } + it do + aggregate_failures do + is_expected.to have_body_text('<script type="application/ld+json">') + is_expected.to have_body_text('ViewAction') + end + end end shared_examples 'it should not have Gmail Actions links' do - it { is_expected.not_to have_body_text '<script type="application/ld+json">' } - it { is_expected.not_to have_body_text /ViewAction/ } + it do + aggregate_failures do + is_expected.not_to have_body_text('<script type="application/ld+json">') + is_expected.not_to have_body_text('ViewAction') + end + end end shared_examples 'it should show Gmail Actions View Issue link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Issue/ } + it { is_expected.to have_body_text('View Issue') } end shared_examples 'it should show Gmail Actions View Merge request link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Merge request/ } + it { is_expected.to have_body_text('View Merge request') } end shared_examples 'it should show Gmail Actions View Commit link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Commit/ } + it { is_expected.to have_body_text('View Commit') } end shared_examples 'an unsubscribeable thread' do it_behaves_like 'an unsubscribeable thread with incoming address without %{key}' - it 'has a List-Unsubscribe header in the correct format' do - is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.to have_header 'List-Unsubscribe', /mailto/ - is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/ + it 'has a List-Unsubscribe header in the correct format, and a body link' do + aggregate_failures do + is_expected.to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.to have_header('List-Unsubscribe', /mailto/) + is_expected.to have_header('List-Unsubscribe', /^<.+,.+>$/) + is_expected.to have_body_text('unsubscribe') + end end - - it { is_expected.to have_body_text /unsubscribe/ } end shared_examples 'an unsubscribeable thread with incoming address without %{key}' do include_context 'reply-by-email is enabled with incoming address without %{key}' - it 'has a List-Unsubscribe header in the correct format' do - is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.not_to have_header 'List-Unsubscribe', /mailto/ - is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/ + it 'has a List-Unsubscribe header in the correct format, and a body link' do + aggregate_failures do + is_expected.to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.not_to have_header('List-Unsubscribe', /mailto/) + is_expected.to have_header('List-Unsubscribe', /^<[^,]+>$/) + is_expected.to have_body_text('unsubscribe') + end end - - it { is_expected.to have_body_text /unsubscribe/ } end shared_examples 'a user cannot unsubscribe through footer link' do - it 'does not have a List-Unsubscribe header' do - is_expected.not_to have_header 'List-Unsubscribe', /unsubscribe/ + it 'does not have a List-Unsubscribe header or a body link' do + aggregate_failures do + is_expected.not_to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.not_to have_body_text('unsubscribe') + end end - - it { is_expected.not_to have_body_text /unsubscribe/ } end shared_examples 'an email with a labels subscriptions link in its footer' do - it { is_expected.to have_body_text /label subscriptions/ } + it { is_expected.to have_body_text('label subscriptions') } end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb index a52d8f37d14..cc79b11616a 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/prometheus_helpers.rb @@ -1,10 +1,10 @@ module PrometheusHelpers def prometheus_memory_query(environment_slug) - %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024} end def prometheus_cpu_query(environment_slug) - %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100} end def prometheus_query_url(prometheus_query) diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index 0d526045012..6b1853c2364 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -22,4 +22,12 @@ module Select2Helper execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');") end end + + def open_select2(selector) + execute_script("$('#{selector}').select2('open');") + end + + def scroll_select2_to_bottom(selector) + evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');" + 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 81d06dc2a3d..ee492daee30 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 @@ -2,7 +2,7 @@ # It can take a `default_params`. shared_examples 'new issuable record that supports slash commands' do - let!(:project) { create(:project) } + let!(:project) { create(:project, :repository) } let(:user) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } let!(:milestone) { create(:milestone, project: project) } diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 704922b6cf4..b902fe90707 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -324,5 +324,24 @@ RSpec.shared_examples 'slack or mattermost notifications' do it_behaves_like 'call Slack/Mattermost API' end end + + context 'only notify for the default branch' do + context 'when enabled' do + let(:pipeline) do + create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch') + end + + before do + chat_service.notify_only_default_branch = true + end + + it 'does not call the Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = chat_service.execute(data) + + expect(result).to be_falsy + end + end + end end end diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb new file mode 100644 index 00000000000..df18926d58c --- /dev/null +++ b/spec/support/stored_repositories.rb @@ -0,0 +1,5 @@ +RSpec.configure do |config| + config.before(:each, :repository) do + TestEnv.clean_test_path + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index f40ee862df8..444adcc1906 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -21,6 +21,10 @@ module StubConfiguration allow(Gitlab.config.incoming_email).to receive_messages(messages) end + def stub_mattermost_setting(messages) + allow(Gitlab.config.mattermost).to receive_messages(messages) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb new file mode 100644 index 00000000000..3ee8f0f657e --- /dev/null +++ b/spec/support/target_branch_helpers.rb @@ -0,0 +1,16 @@ +module TargetBranchHelpers + def select_branch(name) + first('button.js-target-branch').click + wait_for_ajax + all('a[data-group="Branches"]').find do |el| + el.text == name + end.click + end + + def create_new_branch(name) + first('button.js-target-branch').click + click_link 'Create new branch' + fill_in 'new_branch_name', with: name + click_button 'Create' + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f1d226b6ae3..1b5cb71a6b0 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -37,7 +37,8 @@ module TestEnv 'conflict-too-large' => '39fa04f', 'deleted-image-test' => '6c17798', 'wip' => 'b9238ee', - 'csv' => '3dd0896' + 'csv' => '3dd0896', + 'v1.1.0' => 'b83d6e3' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily @@ -60,9 +61,6 @@ module TestEnv clean_test_path - FileUtils.mkdir_p(repos_path) - FileUtils.mkdir_p(backup_path) - # Setup GitLab shell for test instance setup_gitlab_shell @@ -94,10 +92,14 @@ module TestEnv tmp_test_path = Rails.root.join('tmp', 'tests', '**') Dir[tmp_test_path].each do |entry| - unless File.basename(entry) =~ /\Agitlab-(shell|test|test-fork)\z/ + unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/ FileUtils.rm_rf(entry) end end + + FileUtils.mkdir_p(repos_path) + FileUtils.mkdir_p(backup_path) + FileUtils.mkdir_p(pages_path) end def setup_gitlab_shell @@ -129,8 +131,10 @@ module TestEnv set_repo_refs(repo_path, branch_sha) - # We must copy bare repositories because we will push to them. - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) + unless File.directory?(repo_path_bare) + # We must copy bare repositories because we will push to them. + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) + end end def copy_repo(project) @@ -150,6 +154,10 @@ module TestEnv Gitlab.config.backup.path end + def pages_path + Gitlab.config.pages.path + end + def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") @@ -163,16 +171,11 @@ module TestEnv # # Otherwise they'd be created by the first test, often timing out and # causing a transient test failure - def warm_asset_cache - return if warm_asset_cache? + def eager_load_driver_server return unless defined?(Capybara) - Capybara.current_session.driver.visit '/' - end - - def warm_asset_cache? - cache = Rails.root.join(*%w(tmp cache assets test)) - Dir.exist?(cache) && Dir.entries(cache).length > 2 + puts "Starting the Capybara driver server..." + Capybara.current_session.visit '/' end private diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb new file mode 100644 index 00000000000..0bfa7f72ff8 --- /dev/null +++ b/spec/support/wait_for_requests.rb @@ -0,0 +1,32 @@ +module WaitForRequests + extend self + + # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests + def wait_for_requests_complete + Gitlab::Testing::RequestBlockerMiddleware.block_requests! + wait_for('pending AJAX requests complete') do + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + end + ensure + Gitlab::Testing::RequestBlockerMiddleware.allow_requests! + end + + # Waits until the passed block returns true + def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01) + wait_until = Time.now + max_wait_time.seconds + loop do + break if yield + if Time.now > wait_until + raise "Condition not met: #{condition_name}" + else + sleep(polling_interval) + end + end + end +end + +RSpec.configure do |config| + config.after(:each, :js) do + wait_for_requests_complete + end +end diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb index 1029f84716f..4a4e2e16ee7 100644 --- a/spec/support/wait_for_vue_resource.rb +++ b/spec/support/wait_for_vue_resource.rb @@ -1,7 +1,7 @@ module WaitForVueResource def wait_for_vue_resource(spinner: true) Timeout.timeout(Capybara.default_max_wait_time) do - loop until page.evaluate_script('Vue.activeResources').zero? + loop until page.evaluate_script('window.activeVueResources').zero? end end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 10458966cb9..daea0c6bb37 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -81,6 +81,10 @@ describe 'gitlab:app namespace rake task' do end # backup_restore task describe 'backup' do + before(:all) do + ENV['force'] = 'yes' + end + def tars_glob Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) end @@ -88,6 +92,9 @@ describe 'gitlab:app namespace rake task' do def create_backup FileUtils.rm tars_glob + # This reconnect makes our project fixture disappear, breaking the restore. Stub it out. + allow(ActiveRecord::Base.connection).to receive(:reconnect!) + # Redirect STDOUT and run the rake task orig_stdout = $stdout $stdout = StringIO.new @@ -109,7 +116,7 @@ describe 'gitlab:app namespace rake task' do end describe 'backup creation and deletion using custom_hooks' do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user_backup_path) { "repositories/#{project.path_with_namespace}" } before(:each) do @@ -119,9 +126,6 @@ describe 'gitlab:app namespace rake task' do FileUtils.mkdir_p(path) FileUtils.touch(File.join(path, "dummy.txt")) - # We need to use the full path instead of the relative one - allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(File.expand_path(Gitlab.config.gitlab_shell.path, Rails.root.to_s)) - ENV["SKIP"] = "db" create_backup end @@ -220,15 +224,15 @@ describe 'gitlab:app namespace rake task' do end context 'multiple repository storages' do - let(:project_a) { create(:project, repository_storage: 'default') } - let(:project_b) { create(:project, repository_storage: 'custom') } + let(:project_a) { create(:project, :repository, repository_storage: 'default') } + let(:project_b) { create(:project, :repository, repository_storage: 'custom') } before do FileUtils.mkdir('tmp/tests/default_storage') FileUtils.mkdir('tmp/tests/custom_storage') storages = { - 'default' => { 'path' => 'tmp/tests/default_storage' }, - 'custom' => { 'path' => 'tmp/tests/custom_storage' } + 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') }, + 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb new file mode 100644 index 00000000000..d95baddf546 --- /dev/null +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -0,0 +1,78 @@ +require 'rake_helper' + +describe 'gitlab:gitaly namespace rake task' do + before :all do + Rake.application.rake_require 'tasks/gitlab/gitaly' + end + + describe 'install' do + let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' } + let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s } + let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" } + + context 'no dir given' do + it 'aborts and display a help message' do + # avoid writing task output to spec progress + allow($stderr).to receive :write + expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly/ + end + end + + context 'when an underlying Git command fail' do + it 'aborts and display a help message' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).and_raise 'Git error' + + expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' + end + end + + describe 'checkout or clone' do + before do + expect(Dir).to receive(:chdir).with(clone_path) + end + + it 'calls checkout_or_clone_tag with the right arguments' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + + run_rake_task('gitlab:gitaly:install', clone_path) + end + end + + describe 'gmake/make' do + before do + FileUtils.mkdir_p(clone_path) + expect(Dir).to receive(:chdir).with(clone_path).and_call_original + end + + context 'gmake is available' do + before do + expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) + end + + it 'calls gmake in the gitaly directory' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) + expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) + + run_rake_task('gitlab:gitaly:install', clone_path) + end + end + + context 'gmake is not available' do + before do + expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) + end + + it 'calls make in the gitaly directory' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) + expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) + + run_rake_task('gitlab:gitaly:install', clone_path) + end + end + end + end +end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 6de66c3cf07..8a66a4aa047 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -9,9 +9,6 @@ describe 'gitlab:workhorse namespace rake task' do let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s } let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" } - before do - allow(ENV).to receive(:[]) - end context 'no dir given' do it 'aborts and display a help message' do diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb new file mode 100644 index 00000000000..19036c7677c --- /dev/null +++ b/spec/tasks/tokens_spec.rb @@ -0,0 +1,21 @@ +require 'rake_helper' + +describe 'tokens rake tasks' do + let!(:user) { create(:user) } + + before do + Rake.application.rake_require 'tasks/tokens' + end + + describe 'reset_all task' do + it 'invokes create_hooks task' do + expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token } + end + end + + describe 'reset_all_email task' do + it 'invokes create_hooks task' do + expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token } + end + end +end diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/builds/_build.html.haml_spec.rb index e141a117731..751482cac42 100644 --- a/spec/views/projects/builds/_build.html.haml_spec.rb +++ b/spec/views/projects/builds/_build.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'projects/ci/builds/_build' do include Devise::Test::ControllerHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) } diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb index 49b20e5b36b..dc2ffc9dc47 100644 --- a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb +++ b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'projects/generic_commit_statuses/_generic_commit_status.html.haml' do include Devise::Test::ControllerHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) } let(:generic_commit_status) { create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) } diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index ec78ac30593..55b64808fb3 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/builds/show', :view do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) do diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index 8bc344bfbf6..cec87dcecc8 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -4,7 +4,7 @@ describe 'projects/commit/_commit_box.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } before do assign(:project, project) diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb index 889d9a38887..8c845251765 100644 --- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'projects/issues/_related_branches' do include Devise::Test::ControllerHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:branch) { project.repository.find_branch('feature') } let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') } diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 6f70b3daf8e..4052dbf8df3 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -4,11 +4,8 @@ describe 'projects/merge_requests/show/_commits.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } - let(:target_project) { create(:project) } - - let(:source_project) do - create(:project, forked_from_project: target_project) - end + let(:target_project) { create(:project, :repository) } + let(:source_project) { create(:project, :repository, forked_from_project: target_project) } let(:merge_request) do create(:merge_request, :simple, diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 3650b22c389..69c7d0cbf28 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -4,8 +4,8 @@ describe 'projects/merge_requests/edit.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } - let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } let(:milestone) { create(:milestone, project: project) } diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 7f123b15194..dc2fcc3e715 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -4,8 +4,8 @@ describe 'projects/merge_requests/show.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } - let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb index 65f9d0125e6..10095ad7694 100644 --- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/pipelines/_stage', :view do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_pipeline, project: project) } let(:stage) { build(:ci_stage, pipeline: pipeline) } diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index c101f6f164d..dca78dec6df 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe 'projects/pipelines/show' do include Devise::Test::ControllerHelpers - let(:project) { create(:project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, user: user) } before do controller.prepend_view_path('app/views/projects') @@ -21,6 +22,7 @@ describe 'projects/pipelines/show' do assign(:project, project) assign(:pipeline, pipeline) + assign(:commit, project.commit) allow(view).to receive(:can?).and_return(true) end @@ -31,6 +33,12 @@ describe 'projects/pipelines/show' do expect(rendered).to have_css('.js-pipeline-graph') expect(rendered).to have_css('.js-grouped-pipeline-dropdown') + # header + expect(rendered).to have_text("##{pipeline.id}") + expect(rendered).to have_css('time', text: pipeline.created_at.strftime("%b %d, %Y")) + expect(rendered).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"])) + expect(rendered).to have_link(pipeline.user.name, href: user_path(pipeline.user)) + # stages expect(rendered).to have_text('Build') expect(rendered).to have_text('Test') diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index c381b1a86df..900f8d4732f 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'projects/tree/show' do include Devise::Test::ControllerHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:repository) { project.repository } before do diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb deleted file mode 100644 index 542e674c150..00000000000 --- a/spec/workers/build_email_worker_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe BuildEmailWorker do - include EmailHelpers - include RepoHelpers - - let(:build) { create(:ci_build) } - let(:user) { create(:user) } - let(:data) { Gitlab::DataBuilder::Build.build(build) } - - subject { BuildEmailWorker.new } - - before do - allow(build).to receive(:execute_hooks).and_return(false) - build.success - end - - describe "#perform" do - it "sends mail" do - subject.perform(build.id, [user.email], data.stringify_keys) - - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include('Build success for') - expect(email.to).to eq([user.email]) - end - - it "gracefully handles an input SMTP error" do - reset_delivered_emails! - allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) - - subject.perform(build.id, [user.email], data.stringify_keys) - - expect(ActionMailer::Base.deliveries.count).to eq(0) - end - end -end diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb index d9497bd486c..39009d9e4b2 100644 --- a/spec/workers/delete_merged_branches_worker_spec.rb +++ b/spec/workers/delete_merged_branches_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DeleteMergedBranchesWorker do subject(:worker) { described_class.new } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } describe "#perform" do it "calls DeleteMergedBranchesService" do diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index f27e413f7b8..8cf2b888f9a 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -5,7 +5,7 @@ describe EmailsOnPushWorker do include EmailHelpers include EmailSpec::Matchers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } let(:recipients) { user.email } diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index a60af574a08..029f35512e0 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -3,7 +3,7 @@ require 'fileutils' require 'spec_helper' describe GitGarbageCollectWorker do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } subject { GitGarbageCollectWorker.new } diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb index 4e4eaf9b2f7..1ff5a3b9034 100644 --- a/spec/workers/group_destroy_worker_spec.rb +++ b/spec/workers/group_destroy_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe GroupDestroyWorker do let(:group) { create(:group) } let(:user) { create(:admin) } - let!(:project) { create(:project, namespace: group) } + let!(:project) { create(:empty_project, namespace: group) } subject { GroupDestroyWorker.new } diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb index 2d47d93acec..5dbc0da95c2 100644 --- a/spec/workers/pipeline_metrics_worker_spec.rb +++ b/spec/workers/pipeline_metrics_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PipelineMetricsWorker do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } let(:pipeline) do diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index 603ae52ed1e..5a7ce2e08c4 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -11,7 +11,7 @@ describe PipelineNotificationWorker do status: status) end - let(:project) { create(:project, public_builds: false) } + let(:project) { create(:project, :repository, public_builds: false) } let(:user) { create(:user) } let(:pusher) { user } let(:watcher) { pusher } diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 7bcb5521202..a2a559a2369 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,7 +4,7 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 75c7fc1efd2..1c383d0514d 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ProcessCommitWorker do let(:worker) { described_class.new } let(:user) { create(:user) } - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public, :repository) } let(:issue) { create(:issue, project: project, author: user) } let(:commit) { project.commit } diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index f4f63b57a5f..c23ffdf99c0 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ProjectCacheWorker do let(:worker) { described_class.new } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:statistics) { project.statistics } describe '#perform' do diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1f4c39eb64a..0ab42f99510 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ProjectDestroyWorker do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:path) { project.repository.path_to_repo } subject { ProjectDestroyWorker.new } diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb index 27727d6abf9..bcd97a4f6ef 100644 --- a/spec/workers/repository_check/batch_worker_spec.rb +++ b/spec/workers/repository_check/batch_worker_spec.rb @@ -4,7 +4,7 @@ describe RepositoryCheck::BatchWorker do subject { described_class.new } it 'prefers projects that have never been checked' do - projects = create_list(:project, 3, created_at: 1.week.ago) + projects = create_list(:empty_project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 4.months.ago) projects[2].update_column(:last_repository_check_at, 3.months.ago) @@ -12,7 +12,7 @@ describe RepositoryCheck::BatchWorker do end it 'sorts projects by last_repository_check_at' do - projects = create_list(:project, 3, created_at: 1.week.ago) + projects = create_list(:empty_project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 2.months.ago) projects[1].update_column(:last_repository_check_at, 4.months.ago) projects[2].update_column(:last_repository_check_at, 3.months.ago) @@ -21,7 +21,7 @@ describe RepositoryCheck::BatchWorker do end it 'excludes projects that were checked recently' do - projects = create_list(:project, 3, created_at: 1.week.ago) + projects = create_list(:empty_project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 2.days.ago) projects[1].update_column(:last_repository_check_at, 2.months.ago) projects[2].update_column(:last_repository_check_at, 3.days.ago) @@ -40,7 +40,7 @@ describe RepositoryCheck::BatchWorker do it 'skips projects created less than 24 hours ago' do project = create(:empty_project) project.update_column(:created_at, 23.hours.ago) - + expect(subject.perform).to eq([]) end end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 87521ae408e..7d6a2db2972 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe RepositoryForkWorker do - let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } let(:shell) { Gitlab::Shell.new } subject { RepositoryForkWorker.new } diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index c42f3147b7a..fbb22439f33 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe RepositoryImportWorker do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } subject { described_class.new } diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index 262d6e5a9ab..558ff9109ec 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe UpdateMergeRequestsWorker do include RepoHelpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } subject { described_class.new } |