diff options
| author | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-06-02 17:17:54 +0100 |
|---|---|---|
| committer | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-06-02 17:17:54 +0100 |
| commit | 5a4a08249055022e9e6e30d9ab38c45c1bf71842 (patch) | |
| tree | eda99155dee262f3965a7938c9184e73022cf48a /spec | |
| parent | e591401b0b3f08baf4cd28d0fcb8e184a515fc74 (diff) | |
| parent | aea03d7cdfb882762426dbb5cab805682afe5e2a (diff) | |
| download | gitlab-ce-5a4a08249055022e9e6e30d9ab38c45c1bf71842.tar.gz | |
Merge remote-tracking branch 'origin/master' into fix-realtime-edited-text-for-issues-9-3
Diffstat (limited to 'spec')
145 files changed, 4080 insertions, 643 deletions
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index eff9fab8da2..428bc45b842 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -12,7 +12,7 @@ describe Projects::ArtifactsController do status: 'success') end - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } before do project.team << [user, :developer] @@ -22,16 +22,16 @@ describe Projects::ArtifactsController do describe 'GET download' do it 'sends the artifacts file' do - expect(controller).to receive(:send_file).with(build.artifacts_file.path, disposition: 'attachment').and_call_original + expect(controller).to receive(:send_file).with(job.artifacts_file.path, disposition: 'attachment').and_call_original - get :download, namespace_id: project.namespace, project_id: project, build_id: build + get :download, namespace_id: project.namespace, project_id: project, job_id: job end end describe 'GET browse' do context 'when the directory exists' do it 'renders the browse view' do - get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'other_artifacts_0.1.2' + get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'other_artifacts_0.1.2' expect(response).to render_template('projects/artifacts/browse') end @@ -39,7 +39,7 @@ describe Projects::ArtifactsController do context 'when the directory does not exist' do it 'responds Not Found' do - get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' + get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' expect(response).to be_not_found end @@ -49,7 +49,7 @@ describe Projects::ArtifactsController do describe 'GET file' do context 'when the file exists' do it 'renders the file view' do - get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt' + get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt' expect(response).to render_template('projects/artifacts/file') end @@ -57,7 +57,7 @@ describe Projects::ArtifactsController do context 'when the file does not exist' do it 'responds Not Found' do - get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' + get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' expect(response).to be_not_found end @@ -67,7 +67,7 @@ describe Projects::ArtifactsController do describe 'GET raw' do context 'when the file exists' do it 'serves the file using workhorse' do - get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt' + get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt' send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER] @@ -84,7 +84,7 @@ describe Projects::ArtifactsController do context 'when the file does not exist' do it 'responds Not Found' do - get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' + get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' expect(response).to be_not_found end @@ -92,29 +92,29 @@ describe Projects::ArtifactsController do end describe 'GET latest_succeeded' do - def params_from_ref(ref = pipeline.ref, job = build.name, path = 'browse') + def params_from_ref(ref = pipeline.ref, job_name = job.name, path = 'browse') { namespace_id: project.namespace, project_id: project, ref_name_and_path: File.join(ref, path), - job: job + job: job_name } end - context 'cannot find the build' do + context 'cannot find the job' do shared_examples 'not found' do it { expect(response).to have_http_status(:not_found) } end context 'has no such ref' do before do - get :latest_succeeded, params_from_ref('TAIL', build.name) + get :latest_succeeded, params_from_ref('TAIL', job.name) end it_behaves_like 'not found' end - context 'has no such build' do + context 'has no such job' do before do get :latest_succeeded, params_from_ref(pipeline.ref, 'NOBUILD') end @@ -124,20 +124,20 @@ describe Projects::ArtifactsController do context 'has no path' do before do - get :latest_succeeded, params_from_ref(pipeline.sha, build.name, '') + get :latest_succeeded, params_from_ref(pipeline.sha, job.name, '') end it_behaves_like 'not found' end end - context 'found the build and redirect' do - shared_examples 'redirect to the build' do + context 'found the job and redirect' do + shared_examples 'redirect to the job' do it 'redirects' do - path = browse_namespace_project_build_artifacts_path( + path = browse_namespace_project_job_artifacts_path( project.namespace, project, - build) + job) expect(response).to redirect_to(path) end @@ -151,7 +151,7 @@ describe Projects::ArtifactsController do get :latest_succeeded, params_from_ref('master') end - it_behaves_like 'redirect to the build' + it_behaves_like 'redirect to the job' end context 'with branch name containing slash' do @@ -162,7 +162,7 @@ describe Projects::ArtifactsController do get :latest_succeeded, params_from_ref('improve/awesome') end - it_behaves_like 'redirect to the build' + it_behaves_like 'redirect to the job' end context 'with branch name and path containing slashes' do @@ -170,14 +170,14 @@ describe Projects::ArtifactsController do pipeline.update(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) - get :latest_succeeded, params_from_ref('improve/awesome', build.name, 'file/README.md') + get :latest_succeeded, params_from_ref('improve/awesome', job.name, 'file/README.md') end it 'redirects' do - path = file_namespace_project_build_artifacts_path( + path = file_namespace_project_job_artifacts_path( project.namespace, project, - build, + job, 'README.md') expect(response).to redirect_to(path) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 20f99b209eb..f6840578145 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -57,6 +57,11 @@ describe Projects::EnvironmentsController do expect(json_response['available_count']).to eq 3 expect(json_response['stopped_count']).to eq 1 end + + it 'sets the polling interval header' do + expect(response).to have_http_status(:ok) + expect(response.headers['Poll-Interval']).to eq("3000") + end end context 'when requesting stopped environments scope' do @@ -177,7 +182,7 @@ describe Projects::EnvironmentsController do expect(response).to have_http_status(200) expect(json_response).to eq( { 'redirect_url' => - "http://test.host/#{project.path_with_namespace}/builds/#{action.id}" }) + namespace_project_job_url(project.namespace, project, action) }) end end @@ -191,7 +196,7 @@ describe Projects::EnvironmentsController do expect(response).to have_http_status(200) expect(json_response).to eq( { 'redirect_url' => - "http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" }) + namespace_project_environment_url(project.namespace, project, environment) }) end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 04afd07c59e..a38ae2eb990 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -204,7 +204,7 @@ describe Projects::IssuesController do body = JSON.parse(response.body) expect(body['assignees'].first.keys) - .to match_array(%w(id name username avatar_url)) + .to match_array(%w(id name username avatar_url state web_url)) end end diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index f41503fd34e..838bdae1445 100644 --- a/spec/controllers/projects/builds_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::BuildsController do +describe Projects::JobsController do include ApiHelpers let(:project) { create(:empty_project, :public) } @@ -213,7 +213,7 @@ describe Projects::BuildsController do it 'redirects to the retried build page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id)) + expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id)) end end @@ -234,7 +234,11 @@ describe Projects::BuildsController do describe 'POST play' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) + sign_in(user) post_play @@ -245,7 +249,7 @@ describe Projects::BuildsController do it 'redirects to the played build page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_build_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: build.id)) end it 'transits to pending' do @@ -281,7 +285,7 @@ describe Projects::BuildsController do it 'redirects to the canceled build page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_build_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: build.id)) end it 'transits to canceled' do @@ -319,7 +323,7 @@ describe Projects::BuildsController do it 'redirects to a index page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_builds_path) + expect(response).to redirect_to(namespace_project_jobs_path) end it 'transits to canceled' do @@ -336,7 +340,7 @@ describe Projects::BuildsController do it 'redirects to a index page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_builds_path) + expect(response).to redirect_to(namespace_project_jobs_path) end end @@ -359,7 +363,7 @@ describe Projects::BuildsController do it 'redirects to the erased build page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_build_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: build.id)) end it 'erases artifacts' do diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 2d892f4a2b7..23b463c0082 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Projects::ServicesController do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:service) { create(:service, project: project) } + let(:service) { create(:hipchat_service, project: project) } + let(:hipchat_client) { { '#room' => double(send: true) } } + let(:service_params) { { token: 'hipchat_token_p', room: '#room' } } before do sign_in(user) @@ -13,97 +15,81 @@ describe Projects::ServicesController do controller.instance_variable_set(:@service, service) end - shared_examples_for 'services controller' do |referrer| - before do - request.env["HTTP_REFERER"] = referrer - end - - describe "#test" do - context 'when can_test? returns false' do - it 'renders 404' do - allow_any_instance_of(Service).to receive(:can_test?).and_return(false) + describe '#test' do + context 'when can_test? returns false' do + it 'renders 404' do + allow_any_instance_of(Service).to receive(:can_test?).and_return(false) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end + end - context 'success' do - context 'with empty project' do - let(:project) { create(:empty_project) } - - context 'with chat notification service' do - let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') } - - it 'redirects and show success message' do - allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true) - - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + context 'success' do + context 'with empty project' do + let(:project) { create(:empty_project) } - expect(response).to redirect_to(root_path) - expect(flash[:notice]).to eq('We sent a request to the provided URL') - end - end + context 'with chat notification service' do + let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') } - it 'redirects and show success message' do - expect(service).to receive(:test).and_return(success: true, result: 'done') + it 'returns success' do + allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id - expect(response).to redirect_to(root_path) - expect(flash[:notice]).to eq('We sent a request to the provided URL') + expect(response.status).to eq(200) end end - it "redirects and show success message" do - expect(service).to receive(:test).and_return(success: true, result: 'done') + it 'returns success' do + expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params - expect(response).to redirect_to(root_path) - expect(flash[:notice]).to eq('We sent a request to the provided URL') + expect(response.status).to eq(200) end end - context 'failure' do - it "redirects and show failure message" do - expect(service).to receive(:test).and_return(success: false, result: 'Bad test') + it 'returns success' do + expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params - expect(response).to redirect_to(root_path) - expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') - end + expect(response.status).to eq(200) end end - end - describe 'referrer defined' do - it_should_behave_like 'services controller' do - let!(:referrer) { "/" } - end - end + context 'failure' do + it 'returns success status code and the error message' do + expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test') - describe 'referrer undefined' do - it_should_behave_like 'services controller' do - let!(:referrer) { nil } + put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)). + to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test') + end end end describe 'PUT #update' do - context 'on successful update' do - it 'sets the flash' do - expect(service).to receive(:to_param).and_return('hipchat') - expect(service).to receive(:event_names).and_return(HipchatService.event_names) + context 'when param `active` is set to true' do + it 'activates the service and redirects to integrations paths' do + put :update, + namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true } + + expect(response).to redirect_to(namespace_project_settings_integrations_path(project.namespace, project)) + expect(flash[:notice]).to eq 'HipChat activated.' + end + end + context 'when param `active` is set to false' do + it 'does not activate the service but saves the settings' do put :update, - namespace_id: project.namespace.id, - project_id: project.id, - id: service.id, - service: { active: false } + namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false } - expect(flash[:notice]).to eq 'Successfully updated.' + expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.' end end end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb index 007b35bbb77..3cbb173c4cc 100644 --- a/spec/db/production/settings.rb +++ b/spec/db/production/settings.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'rainbow/ext/string' describe 'seed production settings', lib: true do include StubENV diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 361c5b9a49e..03e3c62effe 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -1,5 +1,6 @@ FactoryGirl.define do factory :ci_empty_pipeline, class: Ci::Pipeline do + source :push ref 'master' sha '97de212e80737a608d939f648d959671fb0a0142' status 'pending' diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb index c5fba597c1c..f83366136fd 100644 --- a/spec/factories/ci/variables.rb +++ b/spec/factories/ci/variables.rb @@ -3,6 +3,10 @@ FactoryGirl.define do sequence(:key) { |n| "VARIABLE_#{n}" } value 'VARIABLE_VALUE' + trait(:protected) do + protected true + end + project factory: :empty_project end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 3fad4d2d658..e7366a7fd1c 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -33,4 +33,10 @@ FactoryGirl.define do project_key: 'jira-key' ) end + + factory :hipchat_service do + project factory: :empty_project + type 'HipchatService' + token 'test_token' + end end diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 9d5ce876c29..999ce3611b5 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -16,7 +16,7 @@ describe 'Admin Builds' do create(:ci_build, pipeline: pipeline, status: :success) create(:ci_build, pipeline: pipeline, status: :failed) - visit admin_builds_path + visit admin_jobs_path expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_selector('.row-content-block', text: 'All jobs') @@ -27,7 +27,7 @@ describe 'Admin Builds' do context 'when have no jobs' do it 'shows a message' do - visit admin_builds_path + visit admin_jobs_path expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'No jobs to show' @@ -44,7 +44,7 @@ describe 'Admin Builds' do build3 = create(:ci_build, pipeline: pipeline, status: :success) build4 = create(:ci_build, pipeline: pipeline, status: :failed) - visit admin_builds_path(scope: :pending) + visit admin_jobs_path(scope: :pending) expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page.find('.build-link')).to have_content(build1.id) @@ -59,7 +59,7 @@ describe 'Admin Builds' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) - visit admin_builds_path(scope: :pending) + visit admin_jobs_path(scope: :pending) expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content 'No jobs to show' @@ -76,7 +76,7 @@ describe 'Admin Builds' do build3 = create(:ci_build, pipeline: pipeline, status: :failed) build4 = create(:ci_build, pipeline: pipeline, status: :pending) - visit admin_builds_path(scope: :running) + visit admin_jobs_path(scope: :running) expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page.find('.build-link')).to have_content(build1.id) @@ -91,7 +91,7 @@ describe 'Admin Builds' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) - visit admin_builds_path(scope: :running) + visit admin_jobs_path(scope: :running) expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_content 'No jobs to show' @@ -107,7 +107,7 @@ describe 'Admin Builds' do build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) - visit admin_builds_path(scope: :finished) + visit admin_jobs_path(scope: :finished) expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page.find('.build-link')).not_to have_content(build1.id) @@ -121,7 +121,7 @@ describe 'Admin Builds' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :running) - visit admin_builds_path(scope: :finished) + visit admin_jobs_path(scope: :finished) expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 12cf59f42b0..376e80571d0 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -21,6 +21,8 @@ describe "Admin::Users", feature: true do expect(page).to have_content(current_user.name) expect(page).to have_content(user.email) expect(page).to have_content(user.name) + expect(page).to have_link('Block', href: block_admin_user_path(user)) + expect(page).to have_link('Delete', href: admin_user_path(user)) end describe 'Two-factor Authentication filters' do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index e6c4ab24de5..2772f05982a 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -76,7 +76,7 @@ describe 'Commits' do end end - describe 'Commit builds' do + describe 'Commit builds', :feature, :js do before do visit ci_status_path(pipeline) end @@ -85,7 +85,6 @@ describe 'Commits' 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.user.name - expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y') end end @@ -102,7 +101,7 @@ describe 'Commits' do end describe 'Cancel all builds' do - it 'cancels commit' do + it 'cancels commit', :js do visit ci_status_path(pipeline) click_on 'Cancel running' expect(page).to have_content 'canceled' @@ -110,9 +109,9 @@ describe 'Commits' do end describe 'Cancel build' do - it 'cancels build' do + it 'cancels build', :js do visit ci_status_path(pipeline) - find('a.btn[title="Cancel"]').click + find('.js-btn-cancel-pipeline').click expect(page).to have_content 'canceled' end end @@ -152,17 +151,20 @@ describe 'Commits' do visit ci_status_path(pipeline) end - it do + it 'Renders header', :feature, :js 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.user.name - expect(page).to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry') end + + it do + expect(page).to have_link('Download artifacts') + end end - context 'when accessing internal project with disallowed access' do + context 'when accessing internal project with disallowed access', :feature, :js do before do project.update( visibility_level: Gitlab::VisibilityLevel::INTERNAL, @@ -175,7 +177,7 @@ describe 'Commits' 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.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') end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index b86609e07c5..fa7adbe71ea 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -19,7 +19,7 @@ describe "Container Registry" do scenario 'user visits container registry main page' do visit_container_registry - expect(page).to have_content 'No container image repositories' + expect(page).to have_content 'No container images' end end diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 4d38df05928..44353d880c2 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -157,6 +157,25 @@ describe 'Dropdown assignee', :feature, :js do end end + describe 'selecting from dropdown without Ajax call' do + before do + Gitlab::Testing::RequestBlockerMiddleware.block_requests! + filtered_search.set('assignee:') + end + + after do + Gitlab::Testing::RequestBlockerMiddleware.allow_requests! + end + + it 'selects current user' do + find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect_tokens([{ name: 'assignee', value: user.username }]) + expect_filtered_search_input_empty + end + end + describe 'input has existing content' do it 'opens assignee dropdown with existing search term' do filtered_search.set('searchTerm assignee:') diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 358b244fb5b..6b707c4be4a 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -135,6 +135,25 @@ describe 'Dropdown author', js: true, feature: true do end end + describe 'selecting from dropdown without Ajax call' do + before do + Gitlab::Testing::RequestBlockerMiddleware.block_requests! + filtered_search.set('author:') + end + + after do + Gitlab::Testing::RequestBlockerMiddleware.allow_requests! + end + + it 'selects current user' do + find('#js-dropdown-author .filter-dropdown-item', text: user.username).click + + expect(page).to have_css(js_dropdown_author, visible: false) + expect_tokens([{ name: 'author', value: user.username }]) + expect_filtered_search_input_empty + end + end + describe 'input has existing content' do it 'opens author dropdown with existing search term' do filtered_search.set('searchTerm author:') diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 6c09903a2f6..e75bf059218 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -38,9 +38,11 @@ feature 'issue move to another project' do end scenario 'moving issue to another project', js: true do - find('#move_to_project_id', visible: false).set(new_project.id) + find('#issuable-move', visible: false).set(new_project.id) click_button('Save changes') + wait_for_requests + expect(current_url).to include project_path(new_project) expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}") @@ -51,7 +53,7 @@ feature 'issue move to another project' do scenario 'searching project dropdown', js: true do new_project_search.team << [user, :reporter] - page.within '.js-move-dropdown' do + page.within '.detail-page-description' do first('.select2-choice').click end @@ -69,7 +71,7 @@ feature 'issue move to another project' do background { another_project.team << [user, :guest] } scenario 'browsing projects in projects select' do - click_link 'Select project' + click_link 'Move to a different project' page.within '.select2-results' do expect(page).to have_content 'No project' diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb index 1a09cc54c2e..9db235f35ba 100644 --- a/spec/features/merge_requests/discussion_spec.rb +++ b/spec/features/merge_requests/discussion_spec.rb @@ -5,7 +5,7 @@ feature 'Merge Request Discussions', feature: true do login_as :admin end - context "Diff discussions" do + describe "Diff discussions" do let(:merge_request) { create(:merge_request, importing: true) } let(:project) { merge_request.source_project } let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) } @@ -48,4 +48,43 @@ feature 'Merge Request Discussions', feature: true do end end end + + describe 'Commit comments displayed in MR context', :js do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + + shared_examples 'a functional discussion' do + let(:discussion_id) { note.discussion_id(merge_request) } + + it 'is displayed' do + expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']") + end + + it 'can be replied to' do + within(".discussion[data-discussion-id='#{discussion_id}']") do + click_button 'Reply...' + fill_in 'note[note]', with: 'Test!' + click_button 'Comment' + + expect(page).to have_css('.note', count: 2) + end + end + end + + before(:each) do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'a regular commit comment' do + let(:note) { create(:note_on_commit, project: project) } + + it_behaves_like 'a functional discussion' + end + + context 'a commit diff comment' do + let(:note) { create(:diff_note_on_commit, project: project) } + + it_behaves_like 'a functional discussion' + end + end end diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index 51e7467c14c..3ceb91d951d 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -85,7 +85,7 @@ feature 'Mini Pipeline Graph', :js, :feature do build_item.click find('.build-page') - expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build)) + expect(current_path).to eql(namespace_project_job_path(project.namespace, project, build)) end it 'should show tooltip when hovered' do diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb new file mode 100644 index 00000000000..68375956273 --- /dev/null +++ b/spec/features/projects/artifacts/browse_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Browse artifact', :js, feature: true do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + def browse_path(path) + browse_namespace_project_job_artifacts_path(project.namespace, project, job, path) + end + + context 'when visiting old URL' do + let(:browse_url) do + browse_path('other_artifacts_0.1.2') + end + + before do + visit browse_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(browse_url) + end + end +end diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb new file mode 100644 index 00000000000..dd9454840ee --- /dev/null +++ b/spec/features/projects/artifacts/download_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +feature 'Download artifact', :js, feature: true do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project, sha: project.commit.sha, ref: 'master') } + let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } + + shared_examples 'downloading' do + it 'downloads the zip' do + expect(page.response_headers['Content-Disposition']) + .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"}) + + # Check the content does match, but don't print this as error message + expect(page.source.b == job.artifacts_file.file.read.b) + end + end + + context 'when downloading' do + before do + visit download_url + end + + context 'via job id' do + let(:download_url) do + download_namespace_project_job_artifacts_path(project.namespace, project, job) + end + + it_behaves_like 'downloading' + end + + context 'via branch name and job name' do + let(:download_url) do + latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name) + end + + it_behaves_like 'downloading' + end + end + + context 'when visiting old URL' do + before do + visit download_url.sub('/-/jobs', '/builds') + end + + context 'via job id' do + let(:download_url) do + download_namespace_project_job_artifacts_path(project.namespace, project, job) + end + + it_behaves_like 'downloading' + end + + context 'via branch name and job name' do + let(:download_url) do + latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name) + end + + it_behaves_like 'downloading' + end + end +end diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb index 25db908d917..25c4f3c87a2 100644 --- a/spec/features/projects/artifacts/file_spec.rb +++ b/spec/features/projects/artifacts/file_spec.rb @@ -6,7 +6,11 @@ feature 'Artifact file', :js, feature: true do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } def visit_file(path) - visit file_namespace_project_build_artifacts_path(project.namespace, project, build, path) + visit file_path(path) + end + + def file_path(path) + file_namespace_project_job_artifacts_path(project.namespace, project, build, path) end context 'Text file' do @@ -56,4 +60,18 @@ feature 'Artifact file', :js, feature: true do end end end + + context 'when visiting old URL' do + let(:file_url) do + file_path('other_artifacts_0.1.2/doc_sample.txt') + end + + before do + visit file_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(file_url) + end + end end diff --git a/spec/features/projects/artifacts/raw_spec.rb b/spec/features/projects/artifacts/raw_spec.rb new file mode 100644 index 00000000000..b589701729d --- /dev/null +++ b/spec/features/projects/artifacts/raw_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Raw artifact', :js, feature: true do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + def raw_path(path) + raw_namespace_project_job_artifacts_path(project.namespace, project, job, path) + end + + context 'when visiting old URL' do + let(:raw_url) do + raw_path('other_artifacts_0.1.2/doc_sample.txt') + end + + before do + visit raw_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(raw_url) + end + end +end diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb index d94204230f6..53c5a52ce3a 100644 --- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb +++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb @@ -55,7 +55,7 @@ feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, end end - describe 'Click "Blame" button' do + describe 'Click "Annotate" button' do it 'works with no initial line number fragment hash' do visit_blob diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 86ce50c976f..18b608c863e 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -12,6 +12,7 @@ feature 'Environment', :feature do feature 'environment details page' do given!(:environment) { create(:environment, project: project) } + given!(:permissions) { } given!(:deployment) { } given!(:action) { } @@ -62,20 +63,31 @@ feature 'Environment', :feature do name: 'deploy to production') end - given(:role) { :master } + context 'when user has ability to trigger deployment' do + given(:permissions) do + create(:protected_branch, :developers_can_merge, + name: action.ref, project: project) + end - scenario 'does show a play button' do - expect(page).to have_link(action.name.humanize) - end + it 'does show a play button' do + expect(page).to have_link(action.name.humanize) + end + + it 'does allow to play manual action' do + expect(action).to be_manual - scenario 'does allow to play manual action' do - expect(action).to be_manual + expect { click_link(action.name.humanize) } + .not_to change { Ci::Pipeline.count } - expect { click_link(action.name.humanize) } - .not_to change { Ci::Pipeline.count } + expect(page).to have_content(action.name) + expect(action.reload).to be_pending + end + end - expect(page).to have_content(action.name) - expect(action.reload).to be_pending + context 'when user has no ability to trigger a deployment' do + it 'does not show a play button' do + expect(page).not_to have_link(action.name.humanize) + end end context 'with external_url' do @@ -134,12 +146,23 @@ feature 'Environment', :feature do on_stop: 'close_app') end - given(:role) { :master } + context 'when user has ability to stop environment' do + given(:permissions) do + create(:protected_branch, :developers_can_merge, + name: action.ref, project: project) + end - scenario 'does allow to stop environment' do - click_link('Stop') + it 'allows to stop environment' do + click_link('Stop') - expect(page).to have_content('close_app') + expect(page).to have_content('close_app') + end + end + + context 'when user has no ability to stop environment' do + it 'does not allow to stop environment' do + expect(page).to have_no_link('Stop') + end end context 'for reporter' do @@ -150,12 +173,6 @@ feature 'Environment', :feature do end end end - - context 'without stop action' do - scenario 'does allow to stop environment' do - click_link('Stop') - end - end end context 'when environment is stopped' do diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index c0a9327249c..30a1eedbb48 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -12,7 +12,7 @@ feature 'user browses project', feature: true, js: true do scenario "can see blame of '.gitignore'" do click_link ".gitignore" - click_link 'Blame' + click_link 'Annotate' expect(page).to have_content "*.rb" expect(page).to have_content "Dmitriy Zaporozhets" diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/jobs_spec.rb index 8f4dfa7c48b..0eda46649db 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'tempfile' -feature 'Builds', :feature do +feature 'Jobs', :feature do let(:user) { create(:user) } let(:user_access_level) { :developer } let(:project) { create(:project) } @@ -19,12 +19,12 @@ feature 'Builds', :feature do login_as(user) end - describe "GET /:project/builds" do + describe "GET /:project/jobs" do let!(:build) { create(:ci_build, pipeline: pipeline) } context "Pending scope" do before do - visit namespace_project_builds_path(project.namespace, project, scope: :pending) + visit namespace_project_jobs_path(project.namespace, project, scope: :pending) end it "shows Pending tab jobs" do @@ -39,7 +39,7 @@ feature 'Builds', :feature do context "Running scope" do before do build.run! - visit namespace_project_builds_path(project.namespace, project, scope: :running) + visit namespace_project_jobs_path(project.namespace, project, scope: :running) end it "shows Running tab jobs" do @@ -54,7 +54,7 @@ feature 'Builds', :feature do context "Finished scope" do before do build.run! - visit namespace_project_builds_path(project.namespace, project, scope: :finished) + visit namespace_project_jobs_path(project.namespace, project, scope: :finished) end it "shows Finished tab jobs" do @@ -67,7 +67,7 @@ feature 'Builds', :feature do context "All jobs" do before do project.builds.running_or_pending.each(&:success) - visit namespace_project_builds_path(project.namespace, project) + visit namespace_project_jobs_path(project.namespace, project) end it "shows All tab jobs" do @@ -78,12 +78,26 @@ feature 'Builds', :feature do expect(page).not_to have_link 'Cancel running' end end + + context "when visiting old URL" do + let(:jobs_url) do + namespace_project_jobs_path(project.namespace, project) + end + + before do + visit jobs_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(jobs_url) + end + end end - describe "POST /:project/builds/:id/cancel_all" do + describe "POST /:project/jobs/:id/cancel_all" do before do build.run! - visit namespace_project_builds_path(project.namespace, project) + visit namespace_project_jobs_path(project.namespace, project) click_link "Cancel running" end @@ -97,10 +111,10 @@ feature 'Builds', :feature do end end - describe "GET /:project/builds/:id" do + describe "GET /:project/jobs/:id" do context "Job from project" do before do - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end it 'shows commit`s data' do @@ -117,7 +131,7 @@ feature 'Builds', :feature do context "Job from other project" do before do - visit namespace_project_build_path(project.namespace, project, build2) + visit namespace_project_job_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } @@ -126,7 +140,7 @@ feature 'Builds', :feature do context "Download artifacts" do before do build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end it 'has button to download artifacts' do @@ -139,7 +153,7 @@ feature 'Builds', :feature do build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end context 'no expire date defined' do @@ -183,10 +197,25 @@ feature 'Builds', :feature do end end + context "when visiting old URL" do + let(:job_url) do + namespace_project_job_path(project.namespace, project, build) + end + + before do + visit job_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(job_url) + end + end + feature 'Raw trace' do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) + + visit namespace_project_job_path(project.namespace, project, build) end it do @@ -198,7 +227,7 @@ feature 'Builds', :feature do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end context 'when job has an initial trace' do @@ -222,7 +251,7 @@ feature 'Builds', :feature do end before do - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end it 'shows variable key and value after click', js: true do @@ -247,17 +276,17 @@ feature 'Builds', :feature do let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'job is complete and not successfull' do + context 'job is complete and not successful' do let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) expect(page).to have_link environment.name end @@ -268,7 +297,7 @@ feature 'Builds', :feature do let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) expect(page).to have_link('latest deployment') end @@ -276,11 +305,11 @@ feature 'Builds', :feature do end end - describe "POST /:project/builds/:id/cancel" do + describe "POST /:project/jobs/:id/cancel" do context "Job from project" do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) click_link "Cancel" end @@ -294,19 +323,19 @@ feature 'Builds', :feature do context "Job from other project" do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) - page.driver.post(cancel_namespace_project_build_path(project.namespace, project, build2)) + visit namespace_project_job_path(project.namespace, project, build) + page.driver.post(cancel_namespace_project_job_path(project.namespace, project, build2)) end it { expect(page.status_code).to eq(404) } end end - describe "POST /:project/builds/:id/retry" do + describe "POST /:project/jobs/:id/retry" do context "Job from project" do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) click_link 'Cancel' page.within('.build-header') do click_link 'Retry job' @@ -322,18 +351,18 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do build.run! - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) click_link 'Cancel' - page.driver.post(retry_namespace_project_build_path(project.namespace, project, build2)) + page.driver.post(retry_namespace_project_job_path(project.namespace, project, build2)) end it { expect(page).to have_http_status(404) } end - context "Build that current user is not allowed to retry" do + context "Job that current user is not allowed to retry" do before do build.run! build.cancel! @@ -341,7 +370,7 @@ feature 'Builds', :feature do logout_direct login_with(create(:user)) - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end it 'does not show the Retry button' do @@ -352,30 +381,30 @@ feature 'Builds', :feature do end end - describe "GET /:project/builds/:id/download" do + describe "GET /:project/jobs/:id/download" do before do build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) click_link 'Download' end context "Build from other project" do before do build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_build_artifacts_path(project.namespace, project, build2) + visit download_namespace_project_job_artifacts_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } end end - describe 'GET /:project/builds/:id/raw', :js do + describe 'GET /:project/jobs/:id/raw', :js do context 'access source' do - context 'build from project' do + context 'job from project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } build.run! - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) find('.js-raw-link-controller').click() end @@ -386,11 +415,11 @@ feature 'Builds', :feature do end end - context 'build from other project' do + context 'job from other project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } build2.run! - visit raw_namespace_project_build_path(project.namespace, project, build2) + visit raw_namespace_project_job_path(project.namespace, project, build2) end it 'sends the right headers' do @@ -410,7 +439,7 @@ feature 'Builds', :feature do allow_any_instance_of(Gitlab::Ci::Trace).to receive(:paths) .and_return(paths) - visit namespace_project_build_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, build) end context 'when build has trace in file', :js do @@ -429,7 +458,7 @@ feature 'Builds', :feature do end end - context 'when build has trace in DB' do + context 'when job has trace in DB' do let(:paths) { [] } it 'sends the right headers' do @@ -437,38 +466,52 @@ feature 'Builds', :feature do end end end + + context "when visiting old URL" do + let(:raw_job_url) do + raw_namespace_project_job_path(project.namespace, project, build) + end + + before do + visit raw_job_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(raw_job_url) + end + end end - describe "GET /:project/builds/:id/trace.json" do - context "Build from project" do + describe "GET /:project/jobs/:id/trace.json" do + context "Job from project" do before do - visit trace_namespace_project_build_path(project.namespace, project, build, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, build, format: :json) end it { expect(page.status_code).to eq(200) } end - context "Build from other project" do + context "Job from other project" do before do - visit trace_namespace_project_build_path(project.namespace, project, build2, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, build2, format: :json) end it { expect(page.status_code).to eq(404) } end end - describe "GET /:project/builds/:id/status" do - context "Build from project" do + describe "GET /:project/jobs/:id/status" do + context "Job from project" do before do - visit status_namespace_project_build_path(project.namespace, project, build) + visit status_namespace_project_job_path(project.namespace, project, build) end it { expect(page.status_code).to eq(200) } end - context "Build from other project" do + context "Job from other project" do before do - visit status_namespace_project_build_path(project.namespace, project, build2) + visit status_namespace_project_job_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index cfac54ef259..36a3ddca6ef 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -229,7 +229,6 @@ describe 'Pipeline', :feature, :js do before { find('.js-retry-button').trigger('click') } it { expect(page).not_to have_content('Retry') } - it { expect(page).to have_selector('.retried') } end end @@ -240,7 +239,6 @@ describe 'Pipeline', :feature, :js do before { click_on 'Cancel running' } it { expect(page).not_to have_content('Cancel running') } - it { expect(page).to have_selector('.ci-canceled') } end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index a97a92aa64f..05c2bf350f1 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -442,6 +442,8 @@ describe 'Pipelines', :feature, :js do it 'creates a new pipeline' do expect { click_on 'Create pipeline' } .to change { Ci::Pipeline.count }.by(1) + + expect(Ci::Pipeline.last).to be_web end end diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb new file mode 100644 index 00000000000..c96d87e5708 --- /dev/null +++ b/spec/features/projects/services/jira_service_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +feature 'Setup Jira service', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:service) { project.create_jira_service } + + let(:url) { 'http://jira.example.com' } + let(:project_url) { 'http://username:password@jira.example.com/rest/api/2/project/GitLabProject' } + + def fill_form(active = true) + check 'Active' if active + + fill_in 'service_url', with: url + fill_in 'service_project_key', with: 'GitLabProject' + fill_in 'service_username', with: 'username' + fill_in 'service_password', with: 'password' + fill_in 'service_jira_issue_transition_id', with: '25' + end + + before do + project.team << [user, :master] + login_as(user) + + visit namespace_project_settings_integrations_path(project.namespace, project) + end + + describe 'user sets and activates Jira Service' do + context 'when Jira connection test succeeds' do + before do + WebMock.stub_request(:get, project_url) + end + + it 'activates the JIRA service' do + click_link('JIRA') + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(page).to have_content('JIRA activated.') + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + end + end + + context 'when Jira connection test fails' do + before do + WebMock.stub_request(:get, project_url).to_return(status: 401) + end + + it 'shows errors when some required fields are not filled in' do + click_link('JIRA') + + check 'Active' + fill_in 'service_password', with: 'password' + click_button('Test settings and save changes') + + page.within('.service-settings') do + expect(page).to have_content('This field is required.') + end + end + + it 'activates the JIRA service' do + click_link('JIRA') + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(find('.flash-container-page')).to have_content 'Test failed.' + expect(find('.flash-container-page')).to have_content 'Save anyway' + + find('.flash-alert .flash-action').trigger('click') + wait_for_requests + + expect(page).to have_content('JIRA activated.') + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + end + end + end + + describe 'user sets Jira Service but keeps it disabled' do + context 'when Jira connection test succeeds' do + it 'activates the JIRA service' do + click_link('JIRA') + fill_form(false) + click_button('Save changes') + + expect(page).to have_content('JIRA settings saved, but not activated.') + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + end + end + end +end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index dc3854262e7..1fe82222e59 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') end - it 'shows the token after saving' do + it 'redirects to the integrations page after saving but not activating' do token = ('a'..'z').to_a.join fill_in 'service_token', with: token - click_on 'Save' + click_on 'Save changes' - value = find_field('service_token').value + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + expect(page).to have_content('Mattermost slash commands settings saved, but not activated.') + end + + it 'redirects to the integrations page after activating' do + token = ('a'..'z').to_a.join + + fill_in 'service_token', with: token + check 'service_active' + click_on 'Save changes' - expect(value).to eq(token) + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + expect(page).to have_content('Mattermost slash commands activated.') end it 'shows the add to mattermost button' do diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index db903a0c8f0..f53b820c460 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do expect(page).to have_content('This service allows users to perform common') end - it 'shows the token after saving' do + it 'redirects to the integrations page after saving but not activating' do fill_in 'service_token', with: 'token' click_on 'Save' - value = find_field('service_token').value + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + expect(page).to have_content('Slack slash commands settings saved, but not activated.') + end + + it 'redirects to the integrations page after activating' do + fill_in 'service_token', with: 'token' + check 'service_active' + click_on 'Save' - expect(value).to eq('token') + expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project)) + expect(page).to have_content('Slack slash commands activated.') end it 'shows the correct trigger url' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 78a76d9c112..2a2655bbdb5 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -334,7 +334,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/builds" do - subject { namespace_project_builds_path(project.namespace, project) } + subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public and internal" do before { project.update(public_builds: true) } @@ -368,7 +368,7 @@ describe "Internal Project Access", feature: true do describe "GET /:project_path/builds/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { namespace_project_build_path(project.namespace, project, build.id) } + subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public and internal" do before { project.update(public_builds: true) } @@ -402,7 +402,7 @@ describe "Internal Project Access", feature: true do describe 'GET /:project_path/builds/:id/trace' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { trace_namespace_project_build_path(project.namespace, project, build.id) } + subject { trace_namespace_project_job_path(project.namespace, project, build.id) } context 'when allowed for public and internal' do before do diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index a66f6e09055..b676c236758 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -330,7 +330,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/builds" do - subject { namespace_project_builds_path(project.namespace, project) } + subject { namespace_project_jobs_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -358,7 +358,7 @@ describe "Private Project Access", feature: true do describe "GET /:project_path/builds/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { namespace_project_build_path(project.namespace, project, build.id) } + subject { namespace_project_job_path(project.namespace, project, build.id) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -391,7 +391,7 @@ describe "Private Project Access", feature: true do describe 'GET /:project_path/builds/:id/trace' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { trace_namespace_project_build_path(project.namespace, project, build.id) } + subject { trace_namespace_project_job_path(project.namespace, project, build.id) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 5cd575500c3..35d5163941e 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -154,7 +154,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/builds" do - subject { namespace_project_builds_path(project.namespace, project) } + subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public" do before { project.update(public_builds: true) } @@ -188,7 +188,7 @@ describe "Public Project Access", feature: true do describe "GET /:project_path/builds/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { namespace_project_build_path(project.namespace, project, build.id) } + subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public" do before { project.update(public_builds: true) } @@ -222,7 +222,7 @@ describe "Public Project Access", feature: true do describe 'GET /:project_path/builds/:id/trace' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } - subject { trace_namespace_project_build_path(project.namespace, project, build.id) } + subject { trace_namespace_project_job_path(project.namespace, project, build.id) } context 'when allowed for public' do before do diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index b83a230c1f8..d0c982919db 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -19,7 +19,7 @@ describe 'Project variables', js: true do end end - it 'adds new variable' do + it 'adds new secret variable' do fill_in('variable_key', with: 'key') fill_in('variable_value', with: 'key value') click_button('Add new variable') @@ -27,6 +27,7 @@ describe 'Project variables', js: true do expect(page).to have_content('Variables were successfully updated.') page.within('.variables-table') do expect(page).to have_content('key') + expect(page).to have_content('No') end end @@ -41,6 +42,19 @@ describe 'Project variables', js: true do end end + it 'adds new protected variable' do + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'value') + check('Protected') + click_button('Add new variable') + + expect(page).to have_content('Variables were successfully updated.') + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('Yes') + end + end + it 'reveals and hides new variable' do fill_in('variable_key', with: 'key') fill_in('variable_value', with: 'key value') @@ -85,7 +99,7 @@ describe 'Project variables', js: true do click_button('Save variable') expect(page).to have_content('Variable was successfully updated.') - expect(project.variables.first.value).to eq('key value') + expect(project.variables(true).first.value).to eq('key value') end it 'edits variable with empty value' do @@ -98,6 +112,34 @@ describe 'Project variables', js: true do click_button('Save variable') expect(page).to have_content('Variable was successfully updated.') - expect(project.variables.first.value).to eq('') + expect(project.variables(true).first.value).to eq('') + end + + it 'edits variable to be protected' do + page.within('.variables-table') do + find('.btn-variable-edit').click + end + + expect(page).to have_content('Update variable') + check('Protected') + click_button('Save variable') + + expect(page).to have_content('Variable was successfully updated.') + expect(project.variables(true).first).to be_protected + end + + it 'edits variable to be unprotected' do + project.variables.first.update(protected: true) + + page.within('.variables-table') do + find('.btn-variable-edit').click + end + + expect(page).to have_content('Update variable') + uncheck('Protected') + click_button('Save variable') + + expect(page).to have_content('Variable was successfully updated.') + expect(project.variables(true).first).not_to be_protected end end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 148adcffe3b..03d98459e8c 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -137,6 +137,13 @@ describe ProjectsFinder do it { is_expected.to eq([public_project]) } end + describe 'filter by owned' do + let(:params) { { owned: true } } + let!(:owned_project) { create(:empty_project, :private, namespace: current_user.namespace) } + + it { is_expected.to eq([owned_project]) } + end + describe 'filter by non_public' do let(:params) { { non_public: true } } before do @@ -146,13 +153,19 @@ describe ProjectsFinder do it { is_expected.to eq([private_project]) } end - describe 'filter by viewable_starred_projects' do + describe 'filter by starred' do let(:params) { { starred: true } } before do current_user.toggle_star(public_project) end it { is_expected.to eq([public_project]) } + + it 'returns only projects the user has access to' do + current_user.toggle_star(private_project) + + is_expected.to eq([public_project]) + end end describe 'sorting' do diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json new file mode 100644 index 00000000000..f6346bd0fb6 --- /dev/null +++ b/spec/fixtures/api/schemas/pipeline_schedule.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "description": { "type": "string" }, + "ref": { "type": "string" }, + "cron": { "type": "string" }, + "cron_timezone": { "type": "string" }, + "next_run_at": { "type": "date" }, + "active": { "type": "boolean" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "last_pipeline": { + "type": ["object", "null"], + "properties": { + "id": { "type": "integer" }, + "sha": { "type": "string" }, + "ref": { "type": "string" }, + "status": { "type": "string" } + }, + "additionalProperties": false + }, + "owner": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + } + }, + "required": [ + "id", "description", "ref", "cron", "cron_timezone", "next_run_at", + "active", "created_at", "updated_at", "owner" + ], + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/pipeline_schedules.json b/spec/fixtures/api/schemas/pipeline_schedules.json new file mode 100644 index 00000000000..173a28d2505 --- /dev/null +++ b/spec/fixtures/api/schemas/pipeline_schedules.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "pipeline_schedule.json" } +} diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 6157abfe339..049475a5408 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe AvatarsHelper do + include ApplicationHelper + let(:user) { create(:user) } describe '#user_avatar' do @@ -18,4 +20,103 @@ describe AvatarsHelper do is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16))) end end + + describe '#user_avatar_without_link' do + let(:options) { { user: user } } + subject { helper.user_avatar_without_link(options) } + + it 'displays user avatar' do + is_expected.to eq image_tag( + avatar_icon(user, 16), + class: 'avatar has-tooltip s16 ', + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body' } + ) + end + + context 'with css_class parameter' do + let(:options) { { user: user, css_class: '.cat-pics' } } + + it 'uses provided css_class' do + is_expected.to eq image_tag( + avatar_icon(user, 16), + class: "avatar has-tooltip s16 #{options[:css_class]}", + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body' } + ) + end + end + + context 'with lazy parameter' do + let(:options) { { user: user, lazy: true } } + + it 'uses data-src instead of src' do + is_expected.to eq image_tag( + '', + class: 'avatar has-tooltip s16 ', + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body', src: avatar_icon(user, 16) } + ) + end + end + + context 'with size parameter' do + let(:options) { { user: user, size: 99 } } + + it 'uses provided size' do + is_expected.to eq image_tag( + avatar_icon(user, options[:size]), + class: "avatar has-tooltip s#{options[:size]} ", + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body' } + ) + end + end + + context 'with url parameter' do + let(:options) { { user: user, url: '/over/the/rainbow.png' } } + + it 'uses provided url' do + is_expected.to eq image_tag( + options[:url], + class: 'avatar has-tooltip s16 ', + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body' } + ) + end + end + + context 'with user_name parameter' do + let(:options) { { user_name: 'Tinky Winky', user_email: 'no@f.un' } } + + context 'with user parameter' do + let(:options) { { user: user, user_name: 'Tinky Winky' } } + + it 'prefers user parameter' do + is_expected.to eq image_tag( + avatar_icon(user, 16), + class: 'avatar has-tooltip s16 ', + alt: "#{user.name}'s avatar", + title: user.name, + data: { container: 'body' } + ) + end + end + + it 'uses user_name and user_email parameter if user is not present' do + is_expected.to eq image_tag( + avatar_icon(options[:user_email], 16), + class: 'avatar has-tooltip s16 ', + alt: "#{options[:user_name]}'s avatar", + title: options[:user_name], + data: { container: 'body' } + ) + end + end + end end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 41b5df12522..bd3a3d24b84 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -118,8 +118,8 @@ describe BlobHelper do Class.new(BlobViewer::Base) do include BlobViewer::ServerSide - self.overridable_max_size = 1.megabyte - self.max_size = 5.megabytes + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes self.type = :rich end end @@ -129,7 +129,7 @@ describe BlobHelper do describe '#blob_render_error_reason' do context 'for error :too_large' do - context 'when the blob size is larger than the absolute max size' do + context 'when the blob size is larger than the absolute size limit' do let(:blob) { fake_blob(size: 10.megabytes) } it 'returns an error message' do @@ -137,7 +137,7 @@ describe BlobHelper do end end - context 'when the blob size is larger than the max size' do + context 'when the blob size is larger than the size limit' do let(:blob) { fake_blob(size: 2.megabytes) } it 'returns an error message' do @@ -168,21 +168,19 @@ describe BlobHelper do controller.params[:id] = File.join('master', blob.path) end - context 'for error :too_large' do - context 'when the max size can be overridden' do - let(:blob) { fake_blob(size: 2.megabytes) } + context 'for error :collapsed' do + let(:blob) { fake_blob(size: 2.megabytes) } - it 'includes a "load it anyway" link' do - expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/) - end + it 'includes a "load it anyway" link' do + expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/) end + end - context 'when the max size cannot be overridden' do - let(:blob) { fake_blob(size: 10.megabytes) } + context 'for error :too_large' do + let(:blob) { fake_blob(size: 10.megabytes) } - it 'does not include a "load it anyway" link' do - expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/) - end + it 'does not include a "load it anyway" link' do + expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/) end context 'when the viewer is rich' do diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index dd6566d25bb..a74615e07f9 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -33,17 +33,17 @@ describe DiffHelper do describe 'diff_options' do it 'returns no collapse false' do - expect(diff_options).to include(no_collapse: false) + expect(diff_options).to include(expanded: false) end - it 'returns no collapse true if expand_all_diffs' do - allow(controller).to receive(:params) { { expand_all_diffs: true } } - expect(diff_options).to include(no_collapse: true) + it 'returns no collapse true if expanded' do + allow(controller).to receive(:params) { { expanded: true } } + expect(diff_options).to include(expanded: true) end it 'returns no collapse true if action name diff_for_path' do allow(controller).to receive(:action_name) { 'diff_for_path' } - expect(diff_options).to include(no_collapse: true) + expect(diff_options).to include(expanded: true) end it 'returns paths if action name diff_for_path and param old path' do @@ -129,6 +129,33 @@ describe DiffHelper do end end + describe '#parallel_diff_discussions' do + let(:discussion) { { 'abc_3_3' => 'comment' } } + let(:diff_file) { double(line_code: 'abc_3_3') } + + before do + helper.instance_variable_set(:@grouped_diff_discussions, discussion) + end + + it 'does not put comments on nonewline lines' do + left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3) + right = Gitlab::Diff::Line.new('\\nonewline', 'new-nonewline', 3, 3, 3) + + result = helper.parallel_diff_discussions(left, right, diff_file) + + expect(result).to eq([nil, nil]) + end + + it 'puts comments on added lines' do + left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3) + right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3) + + result = helper.parallel_diff_discussions(left, right, diff_file) + + expect(result).to eq([nil, 'comment']) + end + end + describe "#diff_match_line" do let(:old_pos) { 40 } let(:new_pos) { 50 } diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 728fdfc85f2..8fcf7f5fa15 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -193,39 +193,14 @@ describe IssuablesHelper do end end - describe '#issuable_app_data' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project, last_edited_by: user, created_at: 3.days.ago, updated_at: 2.days.ago, last_edited_at: 2.days.ago) } - let(:issue_app_data) do - { - endpoint: realtime_changes_namespace_project_issue_path(project.namespace, project, issue), - 'can-update' => "true", - 'issuable-ref' => issue.to_reference || '', - updated_at: issue.updated_at.to_time.iso8601, - updated_by: { - name: user.name, - path: user_path(user) - } - } - end - - before do - allow(helper).to receive(:current_user).and_return(nil) - allow(helper).to receive(:can?).with(nil, :update_issue, issue).and_return(true) - end - - it { expect(helper.issuable_app_data(issue.project, issue)).to eq(issue_app_data) } - end - describe '#updated_at_by' do let(:user) { create(:user) } let(:unedited_issuable) { create(:issue) } let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 2.days.ago, last_edited_at: 2.days.ago) } let(:edited_updated_at_by) do { - updated_at: edited_issuable.updated_at.to_time.iso8601, - updated_by: { + updatedAt: edited_issuable.updated_at.to_time.iso8601, + updatedBy: { name: user.name, path: user_path(user) } diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 278bd1f9179..461908f3fde 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -8,7 +8,7 @@ import '~/breakpoints'; import 'vendor/jquery.nicescroll'; describe('Build', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; preloadFixtures('builds/build-with-artifacts.html.raw'); diff --git a/spec/javascripts/droplab/plugins/ajax_filter_spec.js b/spec/javascripts/droplab/plugins/ajax_filter_spec.js new file mode 100644 index 00000000000..8155d98b543 --- /dev/null +++ b/spec/javascripts/droplab/plugins/ajax_filter_spec.js @@ -0,0 +1,72 @@ +import AjaxCache from '~/lib/utils/ajax_cache'; +import AjaxFilter from '~/droplab/plugins/ajax_filter'; + +describe('AjaxFilter', () => { + let dummyConfig; + const dummyData = 'dummy data'; + let dummyList; + + beforeEach(() => { + dummyConfig = { + endpoint: 'dummy endpoint', + searchKey: 'dummy search key', + }; + dummyList = { + data: [], + list: document.createElement('div'), + }; + + AjaxFilter.hook = { + config: { + AjaxFilter: dummyConfig, + }, + list: dummyList, + }; + }); + + describe('trigger', () => { + let ajaxSpy; + + beforeEach(() => { + spyOn(AjaxCache, 'retrieve').and.callFake(url => ajaxSpy(url)); + spyOn(AjaxFilter, '_loadData'); + + dummyConfig.onLoadingFinished = jasmine.createSpy('spy'); + + const dynamicList = document.createElement('div'); + dynamicList.dataset.dynamic = true; + dummyList.list.appendChild(dynamicList); + }); + + it('calls onLoadingFinished after loading data', (done) => { + ajaxSpy = (url) => { + expect(url).toBe('dummy endpoint?dummy search key='); + return Promise.resolve(dummyData); + }; + + AjaxFilter.trigger() + .then(() => { + expect(dummyConfig.onLoadingFinished.calls.count()).toBe(1); + }) + .then(done) + .catch(done.fail); + }); + + it('does not call onLoadingFinished if Ajax call fails', (done) => { + const dummyError = new Error('My dummy is sick! :-('); + ajaxSpy = (url) => { + expect(url).toBe('dummy endpoint?dummy search key='); + return Promise.reject(dummyError); + }; + + AjaxFilter.trigger() + .then(done.fail) + .catch((error) => { + expect(error).toBe(dummyError); + expect(dummyConfig.onLoadingFinished.calls.count()).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js index f617c4bdffe..6e855530b21 100644 --- a/spec/javascripts/environments/environments_store_spec.js +++ b/spec/javascripts/environments/environments_store_spec.js @@ -123,4 +123,13 @@ describe('Store', () => { expect(store.state.paginationInformation).toEqual(expectedResult); }); }); + + describe('getOpenFolders', () => { + it('should return open folder', () => { + store.storeEnvironments(serverData); + + store.toggleFolder(store.state.environments[1]); + expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]); + }); + }); }); diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/jobs.rb index 320de791b08..dc7dde1138c 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do +describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb index 1ce622fc836..17533443d76 100644 --- a/spec/javascripts/fixtures/raw.rb +++ b/spec/javascripts/fixtures/raw.rb @@ -21,4 +21,10 @@ describe 'Raw files', '(JavaScript fixtures)', type: :controller do store_frontend_fixture(blob.data, example.description) end + + it 'blob/notebook/math.json' do |example| + blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb') + + store_frontend_fixture(blob.data, example.description) + end end diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb new file mode 100644 index 00000000000..554451d1bbf --- /dev/null +++ b/spec/javascripts/fixtures/services.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } + + + render_views + + before(:all) do + clean_frontend_fixtures('services/') + end + + before(:each) do + sign_in(admin) + end + + it 'services/edit_service.html.raw' do |example| + get :edit, + namespace_id: namespace, + project_id: project, + id: service.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js new file mode 100644 index 00000000000..45909d4e70e --- /dev/null +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -0,0 +1,199 @@ +import IntegrationSettingsForm from '~/integrations/integration_settings_form'; + +describe('IntegrationSettingsForm', () => { + const FIXTURE = 'services/edit_service.html.raw'; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + }); + + describe('contructor', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + spyOn(integrationSettingsForm, 'init'); + }); + + it('should initialize form element refs on class object', () => { + // Form Reference + expect(integrationSettingsForm.$form).toBeDefined(); + expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM'); + + // Form Child Elements + expect(integrationSettingsForm.$serviceToggle).toBeDefined(); + expect(integrationSettingsForm.$submitBtn).toBeDefined(); + expect(integrationSettingsForm.$submitBtnLoader).toBeDefined(); + expect(integrationSettingsForm.$submitBtnLabel).toBeDefined(); + }); + + it('should initialize form metadata on class object', () => { + expect(integrationSettingsForm.testEndPoint).toBeDefined(); + expect(integrationSettingsForm.canTestService).toBeDefined(); + }); + }); + + describe('toggleServiceState', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should remove `novalidate` attribute to form when called with `true`', () => { + integrationSettingsForm.toggleServiceState(true); + + expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined(); + }); + + it('should set `novalidate` attribute to form when called with `false`', () => { + integrationSettingsForm.toggleServiceState(false); + + expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined(); + }); + }); + + describe('toggleSubmitBtnLabel', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => { + integrationSettingsForm.canTestService = true; + + integrationSettingsForm.toggleSubmitBtnLabel(true); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Test settings and save changes'); + }); + + it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => { + integrationSettingsForm.canTestService = false; + + integrationSettingsForm.toggleSubmitBtnLabel(false); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + + integrationSettingsForm.toggleSubmitBtnLabel(true); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + + integrationSettingsForm.canTestService = true; + + integrationSettingsForm.toggleSubmitBtnLabel(false); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + }); + }); + + describe('toggleSubmitBtnState', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should disable Save button and show loader animation when called with `true`', () => { + integrationSettingsForm.toggleSubmitBtnState(true); + + expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy(); + expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy(); + }); + + it('should enable Save button and hide loader animation when called with `false`', () => { + integrationSettingsForm.toggleSubmitBtnState(false); + + expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy(); + expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy(); + }); + }); + + describe('testSettings', () => { + let integrationSettingsForm; + let formData; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + formData = integrationSettingsForm.$form.serialize(); + }); + + it('should make an ajax request with provided `formData`', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + expect($.ajax).toHaveBeenCalledWith({ + type: 'PUT', + url: integrationSettingsForm.testEndPoint, + data: formData, + }); + }); + + it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { + const errorMessage = 'Test failed.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.resolve({ error: true, message: errorMessage }); + + const $flashContainer = $('.flash-container'); + expect($flashContainer.find('.flash-text').text()).toEqual(errorMessage); + expect($flashContainer.find('.flash-action')).toBeDefined(); + expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway'); + }); + + it('should submit form if ajax request responds without any error in test', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + spyOn(integrationSettingsForm.$form, 'submit'); + deferred.resolve({ error: false }); + + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + }); + + it('should submit form when clicked on `Save anyway` action of error Flash', () => { + const errorMessage = 'Test failed.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.resolve({ error: true, message: errorMessage }); + + const $flashAction = $('.flash-container .flash-action'); + expect($flashAction).toBeDefined(); + + spyOn(integrationSettingsForm.$form, 'submit'); + $flashAction.trigger('click'); + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + }); + + it('should show error Flash if ajax request failed', () => { + const errorMessage = 'Something went wrong on our end.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.reject(); + + expect($('.flash-container .flash-text').text()).toEqual(errorMessage); + }); + + it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); + deferred.reject(); + + expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 28c0ecc10a4..59c006aa0af 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import '~/render_math'; import '~/render_gfm'; import issuableApp from '~/issue_show/components/app.vue'; +import eventHub from '~/issue_show/event_hub'; import issueShowData from '../mock_data'; const issueShowInterceptor = data => (request, next) => { @@ -26,14 +27,25 @@ describe('Issuable output', () => { const IssuableDescriptionComponent = Vue.extend(issuableApp); Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + spyOn(eventHub, '$emit'); + vm = new IssuableDescriptionComponent({ propsData: { canUpdate: true, + canDestroy: true, + canMove: true, endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', issuableRef: '#1', - initialTitle: '', + initialTitleHtml: '', + initialTitleText: '', initialDescriptionHtml: '', initialDescriptionText: '', + markdownPreviewUrl: '/', + markdownDocs: '/', + projectsAutocompleteUrl: '/', + isConfidential: false, + projectNamespace: '/', + projectPath: '/', }, }).$mount(); }); @@ -70,4 +82,296 @@ describe('Issuable output', () => { }); }); }); + + it('shows actions if permissions are correct', (done) => { + vm.showForm = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does not show actions if permissions are incorrect', (done) => { + vm.showForm = true; + vm.canUpdate = false; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn'), + ).toBeNull(); + + done(); + }); + }); + + it('does not update formState if form is already open', (done) => { + vm.openForm(); + + vm.state.titleText = 'testing 123'; + + vm.openForm(); + + Vue.nextTick(() => { + expect( + vm.store.formState.title, + ).not.toBe('testing 123'); + + done(); + }); + }); + + describe('updateIssuable', () => { + it('fetches new data after update', (done) => { + spyOn(vm.service, 'getData'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + confidential: false, + web_url: location.pathname, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.getData, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('reloads the page if the confidential status has changed', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + confidential: true, + web_url: location.pathname, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith(location.pathname); + + done(); + }); + }); + + it('correctly updates issuable data', (done) => { + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve(); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.updateIssuable, + ).toHaveBeenCalledWith(vm.formState); + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + + done(); + }); + }); + + it('does not redirect if issue has not moved', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + web_url: location.pathname, + confidential: vm.isConfidential, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('redirects if issue is moved', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith('/testing-issue-move'); + + done(); + }); + }); + + it('does not update issuable if project move confirm is false', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(vm.service, 'updateIssuable'); + + vm.store.formState.move_to_project_id = 1; + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.updateIssuable, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('closes form on error', (done) => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating issue'); + + done(); + }); + }); + }); + + describe('deleteIssuable', () => { + it('changes URL when deleted', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { web_url: '/test' }; + }, + }); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith('/test'); + + done(); + }); + }); + + it('stops polling when deleting', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.poll, 'stop'); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { web_url: '/test' }; + }, + }); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + vm.poll.stop, + ).toHaveBeenCalledWith(); + + done(); + }); + }); + + it('closes form on error', (done) => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error deleting issue'); + + done(); + }); + }); + }); + + describe('open form', () => { + it('shows locked warning if form is open & data is different', (done) => { + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + + Vue.nextTick() + .then(() => new Promise((resolve) => { + setTimeout(resolve); + })) + .then(() => { + vm.openForm(); + + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); + + return new Promise((resolve) => { + setTimeout(resolve); + }); + }) + .then(() => { + expect( + vm.formState.lockedWarningVisible, + ).toBeTruthy(); + + expect( + vm.$el.querySelector('.alert'), + ).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js new file mode 100644 index 00000000000..f6625b748b6 --- /dev/null +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -0,0 +1,147 @@ +import Vue from 'vue'; +import editActions from '~/issue_show/components/edit_actions.vue'; +import eventHub from '~/issue_show/event_hub'; +import Store from '~/issue_show/stores'; + +describe('Edit Actions components', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(editActions); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + spyOn(eventHub, '$emit'); + + vm = new Component({ + propsData: { + canDestroy: true, + formState: store.formState, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders all buttons as enabled', () => { + expect( + vm.$el.querySelectorAll('.disabled').length, + ).toBe(0); + + expect( + vm.$el.querySelectorAll('[disabled]').length, + ).toBe(0); + }); + + it('does not render delete button if canUpdate is false', (done) => { + vm.canDestroy = false; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-danger'), + ).toBeNull(); + + done(); + }); + }); + + it('disables submit button when title is blank', (done) => { + vm.formState.title = ''; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save').getAttribute('disabled'), + ).toBe('disabled'); + + done(); + }); + }); + + describe('updateIssuable', () => { + it('sends update.issauble event when clicking save button', () => { + vm.$el.querySelector('.btn-save').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('update.issuable'); + }); + + it('shows loading icon after clicking save button', (done) => { + vm.$el.querySelector('.btn-save').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save .fa'), + ).not.toBeNull(); + + done(); + }); + }); + + it('disabled button after clicking save button', (done) => { + vm.$el.querySelector('.btn-save').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save').getAttribute('disabled'), + ).toBe('disabled'); + + done(); + }); + }); + }); + + describe('closeForm', () => { + it('emits close.form when clicking cancel', () => { + vm.$el.querySelector('.btn-default').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + }); + }); + + describe('deleteIssuable', () => { + it('sends delete.issuable event when clicking save button', () => { + spyOn(window, 'confirm').and.returnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('delete.issuable'); + }); + + it('shows loading icon after clicking delete button', (done) => { + spyOn(window, 'confirm').and.returnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-danger .fa'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does no actions when confirm is false', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect( + eventHub.$emit, + ).not.toHaveBeenCalledWith('delete.issuable'); + expect( + vm.$el.querySelector('.btn-danger .fa'), + ).toBeNull(); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js new file mode 100644 index 00000000000..f5b35b1e8b0 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import Store from '~/issue_show/stores'; +import descriptionField from '~/issue_show/components/fields/description.vue'; + +describe('Description field component', () => { + let vm; + let store; + + beforeEach((done) => { + const Component = Vue.extend(descriptionField); + const el = document.createElement('div'); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.description = 'test'; + + document.body.appendChild(el); + + vm = new Component({ + el, + propsData: { + markdownPreviewUrl: '/', + markdownDocs: '/', + formState: store.formState, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders markdown field with description', () => { + expect( + vm.$el.querySelector('.md-area textarea').value, + ).toBe('test'); + }); + + it('renders markdown field with a markdown description', (done) => { + store.formState.description = '**test**'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.md-area textarea').value, + ).toBe('**test**'); + + done(); + }); + }); + + it('focuses field when mounted', () => { + expect( + document.activeElement, + ).toBe(vm.$refs.textarea); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js new file mode 100644 index 00000000000..2b7ee65094b --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; +import '~/templates/issuable_template_selector'; +import '~/templates/issuable_template_selectors'; + +describe('Issue description template component', () => { + let vm; + let formState; + + beforeEach((done) => { + const Component = Vue.extend(descriptionTemplate); + formState = { + description: 'test', + }; + + vm = new Component({ + propsData: { + formState, + issuableTemplates: [{ name: 'test' }], + projectPath: '/', + projectNamespace: '/', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders templates as JSON array in data attribute', () => { + expect( + vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'), + ).toBe('[{"name":"test"}]'); + }); + + it('updates formState when changing template', () => { + vm.issuableTemplate.editor.setValue('test new template'); + + expect( + formState.description, + ).toBe('test new template'); + }); + + it('returns formState description with editor getValue', () => { + formState.description = 'testing new template'; + + expect( + vm.issuableTemplate.editor.getValue(), + ).toBe('testing new template'); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js new file mode 100644 index 00000000000..86d35c33ff4 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import projectMove from '~/issue_show/components/fields/project_move.vue'; + +describe('Project move field component', () => { + let vm; + let formState; + + beforeEach((done) => { + const Component = Vue.extend(projectMove); + + formState = { + move_to_project_id: 0, + }; + + vm = new Component({ + propsData: { + formState, + projectsAutocompleteUrl: '/autocomplete', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('mounts select2 element', () => { + expect( + vm.$el.querySelector('.select2-container'), + ).not.toBeNull(); + }); + + it('updates formState on change', () => { + $(vm.$refs['move-dropdown']).val(2).trigger('change'); + + expect( + formState.move_to_project_id, + ).toBe(2); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js new file mode 100644 index 00000000000..53ae038a6a2 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/title_spec.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Store from '~/issue_show/stores'; +import titleField from '~/issue_show/components/fields/title.vue'; + +describe('Title field component', () => { + let vm; + let store; + + beforeEach(() => { + const Component = Vue.extend(titleField); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + vm = new Component({ + propsData: { + formState: store.formState, + }, + }).$mount(); + }); + + it('renders form control with formState title', () => { + expect( + vm.$el.querySelector('.form-control').value, + ).toBe('test'); + }); +}); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js new file mode 100644 index 00000000000..9a85223208c --- /dev/null +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import formComponent from '~/issue_show/components/form.vue'; +import '~/templates/issuable_template_selector'; +import '~/templates/issuable_template_selectors'; + +describe('Inline edit form component', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(formComponent); + + vm = new Component({ + propsData: { + canDestroy: true, + canMove: true, + formState: { + title: 'b', + description: 'a', + lockedWarningVisible: false, + }, + markdownPreviewUrl: '/', + markdownDocs: '/', + projectsAutocompleteUrl: '/', + projectPath: '/', + projectNamespace: '/', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('does not render template selector if no templates exist', () => { + expect( + vm.$el.querySelector('.js-issuable-selector-wrap'), + ).toBeNull(); + }); + + it('renders template selector when templates exists', (done) => { + spyOn(gl, 'IssuableTemplateSelectors'); + vm.issuableTemplates = ['test']; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-issuable-selector-wrap'), + ).not.toBeNull(); + + done(); + }); + }); + + it('hides locked warning by default', () => { + expect( + vm.$el.querySelector('.alert'), + ).toBeNull(); + }); + + it('shows locked warning if formState is different', (done) => { + vm.formState.lockedWarningVisible = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.alert'), + ).not.toBeNull(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index 2f953e7e92e..a2d90a9b9f5 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Store from '~/issue_show/stores'; import titleComponent from '~/issue_show/components/title.vue'; describe('Title component', () => { @@ -6,11 +7,18 @@ describe('Title component', () => { beforeEach(() => { const Component = Vue.extend(titleComponent); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); vm = new Component({ propsData: { issuableRef: '#1', titleHtml: 'Testing <img />', titleText: 'Testing', + showForm: false, + formState: store.formState, }, }).$mount(); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index e9bffd74d90..e3938a77680 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -356,7 +356,7 @@ import '~/lib/utils/common_utils'; describe('gl.utils.setCiStatusFavicon', () => { it('should set page favicon to CI status favicon based on provided status', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; const FAVICON_PATH = '//icon_status_success'; const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 1173fa40947..f444bcaf847 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -13,7 +13,9 @@ import '~/merge_request'; }); it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); - $('input[type=checkbox]').attr('checked', true).trigger('change'); + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', true, true); + $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); return it('submits an ajax request on tasklist:changed', function() { diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index 38c976f38d8..a88e9ed3d99 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; import MarkdownComponent from '~/notebook/cells/markdown.vue'; +import katex from 'vendor/katex'; const Component = Vue.extend(MarkdownComponent); +window.katex = katex; + describe('Markdown component', () => { let vm; let cell; @@ -38,4 +41,58 @@ describe('Markdown component', () => { it('renders the markdown HTML', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); + + describe('katex', () => { + beforeEach(() => { + json = getJSONFixture('blob/notebook/math.json'); + }); + + it('renders multi-line katex', (done) => { + vm = new Component({ + propsData: { + cell: json.cells[0], + }, + }).$mount(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.katex'), + ).not.toBeNull(); + + done(); + }); + }); + + it('renders inline katex', (done) => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('p:first-child .katex'), + ).not.toBeNull(); + + done(); + }); + }); + + it('renders multiple inline katex', (done) => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('p:nth-child(2) .katex').length, + ).toBe(4); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 04cf0fe2bf8..17aa70ff3f1 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -34,7 +34,9 @@ import '~/notes'; }); it('modifies the Markdown field', function() { - $('input[type=checkbox]').attr('checked', true).trigger('change'); + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', true, true); + $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index f033956c071..85bd87318db 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue'; describe('pipeline graph action component', () => { let component; - beforeEach(() => { + beforeEach((done) => { const ActionComponent = Vue.extend(actionComponent); component = new ActionComponent({ propsData: { @@ -14,6 +14,8 @@ describe('pipeline graph action component', () => { actionIcon: 'icon_action_cancel', }, }).$mount(); + + Vue.nextTick(done); }); it('should render a link', () => { @@ -27,7 +29,7 @@ describe('pipeline graph action component', () => { it('should update bootstrap tooltip when title changes', (done) => { component.tooltipText = 'changed'; - Vue.nextTick(() => { + setTimeout(() => { expect(component.$el.getAttribute('data-original-title')).toBe('changed'); done(); }); diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js index 14ff1b0d25c..25fd18b197e 100644 --- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js +++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js @@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio describe('action component', () => { let component; - beforeEach(() => { + beforeEach((done) => { const DropdownActionComponent = Vue.extend(dropdownActionComponent); component = new DropdownActionComponent({ propsData: { @@ -14,6 +14,8 @@ describe('action component', () => { actionIcon: 'icon_action_cancel', }, }).$mount(); + + Vue.nextTick(done); }); it('should render a link', () => { diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 63986b6c0db..e90593e0f40 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -27,26 +27,30 @@ describe('pipeline graph job component', () => { }); describe('name with link', () => { - it('should render the job name and status with a link', () => { + it('should render the job name and status with a link', (done) => { const component = new JobComponent({ propsData: { job: mockJob, }, }).$mount(); - const link = component.$el.querySelector('a'); + Vue.nextTick(() => { + const link = component.$el.querySelector('a'); - expect(link.getAttribute('href')).toEqual(mockJob.status.details_path); + expect(link.getAttribute('href')).toEqual(mockJob.status.details_path); - expect( - link.getAttribute('data-original-title'), - ).toEqual(`${mockJob.name} - ${mockJob.status.label}`); + expect( + link.getAttribute('data-original-title'), + ).toEqual(`${mockJob.name} - ${mockJob.status.label}`); - expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); + expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); - expect( - component.$el.querySelector('.ci-status-text').textContent.trim(), - ).toEqual(mockJob.name); + expect( + component.$el.querySelector('.ci-status-text').textContent.trim(), + ).toEqual(mockJob.name); + + done(); + }); }); }); diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js new file mode 100644 index 00000000000..b8531350e43 --- /dev/null +++ b/spec/javascripts/pipelines/header_component_spec.js @@ -0,0 +1,60 @@ +import Vue from 'vue'; +import headerComponent from '~/pipelines/components/header_component.vue'; +import eventHub from '~/pipelines/event_hub'; + +describe('Pipeline details header', () => { + let HeaderComponent; + let vm; + let props; + + beforeEach(() => { + HeaderComponent = Vue.extend(headerComponent); + + props = { + pipeline: { + details: { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + }, + id: 123, + created_at: '2017-05-08T14:57:39.781Z', + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'path', + }, + isLoading: false, + }; + + vm = new HeaderComponent({ propsData: props }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render provided pipeline info', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo'); + }); + + describe('action buttons', () => { + it('should call postAction when button action is clicked', () => { + eventHub.$on('headerPostAction', (action) => { + expect(action.path).toEqual('path'); + }); + + vm.$el.querySelector('button').click(); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js new file mode 100644 index 00000000000..9fec2f61f78 --- /dev/null +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; + +describe('PipelineMdediator', () => { + let mediator; + beforeEach(() => { + mediator = new PipelineMediator({ endpoint: 'foo' }); + }); + + it('should set defaults', () => { + expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.state.isLoading).toEqual(false); + expect(mediator.store).toBeDefined(); + expect(mediator.service).toBeDefined(); + }); + + describe('request and store data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ foo: 'bar' }), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + }); + + it('should store received data', (done) => { + mediator.fetchPipeline(); + + setTimeout(() => { + expect(mediator.store.state.pipeline).toEqual({ foo: 'bar' }); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/javascripts/pipelines/pipeline_store_spec.js new file mode 100644 index 00000000000..85d13445b01 --- /dev/null +++ b/spec/javascripts/pipelines/pipeline_store_spec.js @@ -0,0 +1,27 @@ +import PipelineStore from '~/pipelines/stores/pipeline_store'; + +describe('Pipeline Store', () => { + let store; + + beforeEach(() => { + store = new PipelineStore(); + }); + + it('should set defaults', () => { + expect(store.state).toEqual({ pipeline: {} }); + expect(store.state.pipeline).toEqual({}); + }); + + describe('storePipeline', () => { + it('should store empty object if none is provided', () => { + store.storePipeline(); + + expect(store.state.pipeline).toEqual({}); + }); + + it('should store received object', () => { + store.storePipeline({ foo: 'bar' }); + expect(store.state.pipeline).toEqual({ foo: 'bar' }); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index d74b1281668..594a9856d2c 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -47,6 +47,7 @@ describe('Pipeline Url Component', () => { web_url: '/', name: 'foo', avatar_url: '/', + path: '/', }, }, }; diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 0638483e7aa..050170a54e9 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -24,6 +24,7 @@ describe('Commit component', () => { author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', username: 'jschatz1', }, }, @@ -46,6 +47,7 @@ describe('Commit component', () => { author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', username: 'jschatz1', }, commitIconSvg: '<svg></svg>', @@ -81,7 +83,7 @@ describe('Commit component', () => { it('should render a link to the author profile', () => { expect( component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), - ).toEqual(props.author.web_url); + ).toEqual(props.author.path); }); it('Should render the author avatar with title and alt attributes', () => { diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 1bf8916b3d0..2b51c89f311 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -33,12 +33,14 @@ describe('Header CI Component', () => { path: 'path', type: 'button', cssClass: 'btn', + isLoading: false, }, { label: 'Go', path: 'path', type: 'link', cssClass: 'link', + isLoading: false, }, ], }; @@ -79,4 +81,13 @@ describe('Header CI Component', () => { expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); }); + + it('should show loading icon', (done) => { + vm.actions[0].isLoading = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual(''); + done(); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js new file mode 100644 index 00000000000..4bbaff561fc --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -0,0 +1,121 @@ +import Vue from 'vue'; +import fieldComponent from '~/vue_shared/components/markdown/field.vue'; + +describe('Markdown field component', () => { + let vm; + + beforeEach(() => { + vm = new Vue({ + render(createElement) { + return createElement( + fieldComponent, + { + props: { + markdownPreviewUrl: '/preview', + markdownDocs: '/docs', + }, + }, + [ + createElement('textarea', { + slot: 'textarea', + }), + ], + ); + }, + }); + }); + + it('creates a new instance of GL form', (done) => { + spyOn(gl, 'GLForm'); + vm.$mount(); + + Vue.nextTick(() => { + expect( + gl.GLForm, + ).toHaveBeenCalled(); + + done(); + }); + }); + + describe('mounted', () => { + beforeEach((done) => { + vm.$mount(); + + Vue.nextTick(done); + }); + + it('renders textarea inside backdrop', () => { + expect( + vm.$el.querySelector('.zen-backdrop textarea'), + ).not.toBeNull(); + }); + + describe('markdown preview', () => { + let previewLink; + + beforeEach(() => { + spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + body: '<p>markdown preview</p>', + }; + }, + }); + })); + + previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a'); + }); + + it('sets preview link as active', (done) => { + previewLink.click(); + + Vue.nextTick(() => { + expect( + previewLink.parentNode.classList.contains('active'), + ).toBeTruthy(); + + done(); + }); + }); + + it('shows preview loading text', (done) => { + previewLink.click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.md-preview').textContent.trim(), + ).toContain('Loading...'); + + done(); + }); + }); + + it('renders markdown preview', (done) => { + previewLink.click(); + + setTimeout(() => { + expect( + vm.$el.querySelector('.md-preview').innerHTML, + ).toContain('<p>markdown preview</p>'); + + done(); + }); + }); + + it('renders GFM with jQuery', (done) => { + spyOn($.fn, 'renderGFM'); + previewLink.click(); + + setTimeout(() => { + expect( + $.fn.renderGFM, + ).toHaveBeenCalled(); + + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js new file mode 100644 index 00000000000..7110ff36937 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js @@ -0,0 +1,67 @@ +import Vue from 'vue'; +import headerComponent from '~/vue_shared/components/markdown/header.vue'; + +describe('Markdown field header component', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(headerComponent); + + vm = new Component({ + propsData: { + previewMarkdown: false, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders markdown buttons', () => { + expect( + vm.$el.querySelectorAll('.js-md').length, + ).toBe(7); + }); + + it('renders `write` link as active when previewMarkdown is false', () => { + expect( + vm.$el.querySelector('li:nth-child(1)').classList.contains('active'), + ).toBeTruthy(); + }); + + it('renders `preview` link as active when previewMarkdown is true', (done) => { + vm.previewMarkdown = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('li:nth-child(2)').classList.contains('active'), + ).toBeTruthy(); + + done(); + }); + }); + + it('emits toggle markdown event when clicking preview', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('li:nth-child(2) a').click(); + + expect( + vm.$emit, + ).toHaveBeenCalledWith('toggle-markdown'); + }); + + it('blurs preview link after click', (done) => { + const link = vm.$el.querySelector('li:nth-child(2) a'); + spyOn(HTMLElement.prototype, 'blur'); + + link.click(); + + setTimeout(() => { + expect( + link.blur, + ).toHaveBeenCalled(); + + done(); + }); + }); +}); 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 286118917e8..67419cfcbea 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -76,7 +76,7 @@ describe('Pipelines Table Row', () => { it('should render user information', () => { expect( component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), - ).toEqual(pipeline.user.web_url); + ).toEqual(pipeline.user.path); expect( component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), @@ -120,7 +120,7 @@ describe('Pipelines Table Row', () => { component = buildComponent(pipeline); const { commitAuthorLink, commitAuthorName } = findElements(); - expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url); + expect(commitAuthorLink).toEqual(pipeline.commit.author.path); expect(commitAuthorName).toEqual(pipeline.commit.author.username); }); diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 63b23dac7ed..edf3846b742 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -16,6 +16,11 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq(exp) end + it 'ignores references with text before the @ sign' do + exp = act = "Hey foo#{reference}" + expect(reference_filter(act).to_html).to eq(exp) + end + %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Hey #{reference}</#{elem}>" diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb new file mode 100644 index 00000000000..1d92a5cb33f --- /dev/null +++ b/spec/lib/feature_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Feature, lib: true do + describe '.get' do + let(:feature) { double(:feature) } + let(:key) { 'my_feature' } + + it 'returns the Flipper feature' do + expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key). + and_return(feature) + + expect(described_class.get(key)).to be(feature) + end + end + + describe '.all' do + let(:features) { Set.new } + + it 'returns the Flipper features as an array' do + expect_any_instance_of(Flipper::DSL).to receive(:features). + and_return(features) + + expect(described_class.all).to eq(features.to_a) + end + end +end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index eb4f06b371c..13e6953147b 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -58,9 +58,12 @@ describe Gitlab::ChatCommands::Command, service: true do end end - context 'and user does have deployment permission' do + context 'and user has deployment permission' do before do - build.project.add_master(user) + build.project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'returns action' do diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index b33389d959e..46dbdeae37c 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -7,7 +7,12 @@ describe Gitlab::ChatCommands::Deploy, service: true do let(:regex_match) { described_class.match('deploy staging to production') } before do - project.add_master(user) + # Make it possible to trigger protected manual actions for developers. + # + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end subject do diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb index 40b96b1807b..72bd7c4eb93 100644 --- a/spec/lib/gitlab/ci/status/build/common_spec.rb +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::Ci::Status::Build::Common do describe '#details_path' do it 'links to the build details page' do - expect(subject.details_path).to include "builds/#{build.id}" + expect(subject.details_path).to include "jobs/#{build.id}" end end end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 185bb9098da..3f30b2c38f2 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -224,7 +224,10 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when user has ability to play action' do before do - build.project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'fabricates status that has action' do diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f5d0f977768..0e15a5f3c6b 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Play do let(:user) { create(:user) } + let(:project) { build.project } let(:build) { create(:ci_build, :manual) } let(:status) { Gitlab::Ci::Status::Core.new(build, user) } @@ -15,8 +16,13 @@ describe Gitlab::Ci::Status::Build::Play do describe '#has_action?' do context 'when user is allowed to update build' do - context 'when user can push to branch' do - before { build.project.add_master(user) } + context 'when user is allowed to trigger protected action' do + before do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 48fc817d857..1482ef7132d 100644 --- a/spec/lib/gitlab/git/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" -describe Gitlab::Git::EncodingHelper do - let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } } +describe Gitlab::EncodingHelper do + let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } } let(:binary_string) { File.read(Rails.root + "spec/fixtures/dk.png") } describe '#encode!' do diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 46a238b17f4..0418fc0a1e2 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -77,6 +77,17 @@ describe Gitlab::EtagCaching::Router do expect(result).to be_blank end + it 'matches the environments path' do + env = build_env( + '/my-group/my-project/environments.json' + ) + + result = described_class.match(env) + expect(result).to be_present + + expect(result.name).to eq 'environments' + end + it 'matches pipeline#show endpoint' do env = build_env( '/my-group/my-project/pipelines/2.json' diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index ae617b313c5..3565e719ad3 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -6,8 +6,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do iterator, max_files: max_files, max_lines: max_lines, - all_diffs: all_diffs, - no_collapse: no_collapse + limits: limits, + expanded: expanded ) end let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) } @@ -16,8 +16,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do let(:line_count) { 1 } let(:max_files) { 10 } let(:max_lines) { 100 } - let(:all_diffs) { false } - let(:no_collapse) { true } + let(:limits) { true } + let(:expanded) { true } describe '#to_a' do subject { super().to_a } @@ -75,7 +75,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -94,7 +94,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do describe '#size' do it { expect(subject.size).to eq(3) } - + it 'does not change after peeking' do subject.any? expect(subject.size).to eq(3) @@ -123,7 +123,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(0) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -167,7 +167,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(10) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -207,7 +207,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(3) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -273,7 +273,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(9) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -344,7 +344,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do let(:iterator) { [{ diff: 'a' * 20480 }] } context 'when no collapse is set' do - let(:no_collapse) { true } + let(:expanded) { true } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) }. @@ -363,7 +363,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when no collapse is unset' do - let(:no_collapse) { false } + let(:expanded) { false } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) }. @@ -450,7 +450,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) }. diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4189aaef643..8e24168ad71 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -85,12 +85,12 @@ EOT # The patch total size is 200, with lines between 21 and 54. # This is a quick-and-dirty way to test this. Ideally, a new patch is # added to the test repo with a size that falls between the real limits. - stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150) - stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100) + stub_const("#{described_class}::SIZE_LIMIT", 150) + stub_const("#{described_class}::COLLAPSE_LIMIT", 100) end it 'prunes the diff as a large diff instead of as a collapsed diff' do - diff = described_class.new(@rugged_diff, collapse: true) + diff = described_class.new(@rugged_diff, expanded: false) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -269,7 +269,7 @@ EOT it 'returns true for a diff that was explicitly marked as being too large' do diff = described_class.new(diff: 'a') - diff.prune_large_diff! + diff.too_large! expect(diff.too_large?).to eq(true) end @@ -291,31 +291,31 @@ EOT it 'returns true for a diff that was explicitly marked as being collapsed' do diff = described_class.new(diff: 'a') - diff.prune_collapsed_diff! + diff.collapse! expect(diff).to be_collapsed end end - describe '#collapsible?' do + describe '#collapsed?' do it 'returns true for a diff that is quite large' do - diff = described_class.new(diff: 'a' * 20480) + diff = described_class.new({ diff: 'a' * 20480 }, expanded: false) - expect(diff).to be_collapsible + expect(diff).to be_collapsed end it 'returns false for a diff that is small enough' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }, expanded: false) - expect(diff).not_to be_collapsible + expect(diff).not_to be_collapsed end end - describe '#prune_collapsed_diff!' do + describe '#collapse!' do it 'prunes the diff' do diff = described_class.new(diff: "foo\nbar") - diff.prune_collapsed_diff! + diff.collapse! expect(diff.diff).to eq('') expect(diff.line_count).to eq(0) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cb107c6d1f9..26215381cc4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do - include Gitlab::Git::EncodingHelper + include Gitlab::EncodingHelper let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } @@ -381,6 +381,19 @@ describe Gitlab::Git::Repository, seed_helper: true do } ]) end + + it 'should not break on invalid syntax' do + allow(repository).to receive(:blob_content).and_return(<<-GITMODULES.strip_heredoc) + [submodule "six"] + path = six + url = git://github.com/randx/six.git + + [submodule] + foo = bar + GITMODULES + + expect(submodules).to have_key('six') + end end context 'where repo doesn\'t have submodules' do diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 08ee0dff6b2..95ecba67532 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper' -describe Gitlab::GitalyClient, lib: true do +# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want +# those stubs while testing the GitalyClient itself. +describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do describe '.stub' do + # Notice that this is referring to gRPC "stubs", not rspec stubs before { described_class.clear_stubs! } context 'when passed a UNIX socket address' do @@ -32,4 +35,81 @@ describe Gitlab::GitalyClient, lib: true do end end end + + describe 'feature_enabled?' do + let(:feature_name) { 'my_feature' } + let(:real_feature_name) { "gitaly_#{feature_name}" } + + context 'when Gitaly is disabled' do + before { allow(described_class).to receive(:enabled?).and_return(false) } + + it 'returns false' do + expect(described_class.feature_enabled?(feature_name)).to be(false) + end + end + + context 'when the feature status is DISABLED' do + let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::DISABLED } + + it 'returns false' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + end + + context 'when the feature_status is OPT_IN' do + let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_IN } + + context "when the feature flag hasn't been set" do + it 'returns false' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + end + + context "when the feature flag is set to disable" do + before { Feature.get(real_feature_name).disable } + + it 'returns false' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + end + + context "when the feature flag is set to enable" do + before { Feature.get(real_feature_name).enable } + + it 'returns true' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) + end + end + + context "when the feature flag is set to a percentage of time" do + before { Feature.get(real_feature_name).enable_percentage_of_time(70) } + + it 'bases the result on pseudo-random numbers' do + expect(Random).to receive(:rand).and_return(0.3) + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) + + expect(Random).to receive(:rand).and_return(0.8) + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + end + end + + context 'when the feature_status is OPT_OUT' do + let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_OUT } + + context "when the feature flag hasn't been set" do + it 'returns true' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) + end + end + + context "when the feature flag is set to disable" do + before { Feature.get(real_feature_name).disable } + + it 'returns false' do + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + 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 96054c996fd..54ce8051f30 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -191,6 +191,7 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id +- source CommitStatus: - id - project_id diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 56772409989..00941aec380 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,5 +1,7 @@ +require 'spec_helper' + describe Gitlab::Utils, lib: true do - delegate :to_boolean, to: :described_class + delegate :to_boolean, :boolean_to_yes_no, to: :described_class describe '.to_boolean' do it 'accepts booleans' do @@ -30,4 +32,11 @@ describe Gitlab::Utils, lib: true do expect(to_boolean(nil)).to be_nil end end + + describe '.boolean_to_yes_no' do + it 'converts booleans to Yes or No' do + expect(boolean_to_yes_no(true)).to eq('Yes') + expect(boolean_to_yes_no(false)).to eq('No') + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index fdbb55fc874..b1999409170 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -244,7 +244,7 @@ describe Gitlab::Workhorse, lib: true do context "when git_receive_pack action is passed" do let(:action) { 'git_receive_pack' } - it { expect(subject).not_to include(gitaly_params) } + it { expect(subject).to include(gitaly_params) } end context "when info_refs action is passed" do diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb new file mode 100644 index 00000000000..a5c6170cd7d --- /dev/null +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -0,0 +1,223 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::SimpleExecutor, lib: true do + class SimpleCheck < SystemCheck::BaseCheck + set_name 'my simple check' + + def check? + true + end + end + + class OtherCheck < SystemCheck::BaseCheck + set_name 'other check' + + def check? + false + end + + def show_error + $stdout.puts 'this is an error text' + end + end + + class SkipCheck < SystemCheck::BaseCheck + set_name 'skip check' + set_skip_reason 'this is a skip reason' + + def skip? + true + end + + def check? + raise 'should not execute this' + end + end + + class MultiCheck < SystemCheck::BaseCheck + set_name 'multi check' + + def multi_check + $stdout.puts 'this is a multi output check' + end + + def check? + raise 'should not execute this' + end + end + + class SkipMultiCheck < SystemCheck::BaseCheck + set_name 'skip multi check' + + def skip? + true + end + + def multi_check + raise 'should not execute this' + end + end + + class RepairCheck < SystemCheck::BaseCheck + set_name 'repair check' + + def check? + false + end + + def repair! + true + end + + def show_error + $stdout.puts 'this is an error message' + end + end + + describe '#component' do + it 'returns stored component name' do + expect(subject.component).to eq('Test') + end + end + + describe '#checks' do + before do + subject << SimpleCheck + end + + it 'returns a set of classes' do + expect(subject.checks).to include(SimpleCheck) + end + end + + describe '#<<' do + before do + subject << SimpleCheck + end + + it 'appends a new check to the Set' do + subject << OtherCheck + stored_checks = subject.checks.to_a + + expect(stored_checks.first).to eq(SimpleCheck) + expect(stored_checks.last).to eq(OtherCheck) + end + + it 'inserts unique itens only' do + subject << SimpleCheck + + expect(subject.checks.size).to eq(1) + end + end + + subject { described_class.new('Test') } + + describe '#execute' do + before do + silence_output + + subject << SimpleCheck + subject << OtherCheck + end + + it 'runs included checks' do + expect(subject).to receive(:run_check).with(SimpleCheck) + expect(subject).to receive(:run_check).with(OtherCheck) + + subject.execute + end + end + + describe '#run_check' do + it 'prints check name' do + expect(SimpleCheck).to receive(:display_name).and_call_original + expect { subject.run_check(SimpleCheck) }.to output(/my simple check/).to_stdout + end + + context 'when check pass' do + it 'prints yes' do + expect_any_instance_of(SimpleCheck).to receive(:check?).and_call_original + expect { subject.run_check(SimpleCheck) }.to output(/ \.\.\. yes/).to_stdout + end + end + + context 'when check fails' do + it 'prints no' do + expect_any_instance_of(OtherCheck).to receive(:check?).and_call_original + expect { subject.run_check(OtherCheck) }.to output(/ \.\.\. no/).to_stdout + end + + it 'displays error message from #show_error' do + expect_any_instance_of(OtherCheck).to receive(:show_error).and_call_original + expect { subject.run_check(OtherCheck) }.to output(/this is an error text/).to_stdout + end + + context 'when check implements #repair!' do + it 'executes #repair!' do + expect_any_instance_of(RepairCheck).to receive(:repair!) + + subject.run_check(RepairCheck) + end + + context 'when repair succeeds' do + it 'does not execute #show_error' do + expect_any_instance_of(RepairCheck).to receive(:repair!).and_call_original + expect_any_instance_of(RepairCheck).not_to receive(:show_error) + + subject.run_check(RepairCheck) + end + end + + context 'when repair fails' do + it 'does not execute #show_error' do + expect_any_instance_of(RepairCheck).to receive(:repair!) { false } + expect_any_instance_of(RepairCheck).to receive(:show_error) + + subject.run_check(RepairCheck) + end + end + end + end + + context 'when check implements skip?' do + it 'executes #skip? method' do + expect_any_instance_of(SkipCheck).to receive(:skip?).and_call_original + + subject.run_check(SkipCheck) + end + + it 'displays #skip_reason' do + expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout + end + + it 'does not execute #check when #skip? is true' do + expect_any_instance_of(SkipCheck).not_to receive(:check?) + + subject.run_check(SkipCheck) + end + end + + context 'when implements a #multi_check' do + it 'executes #multi_check method' do + expect_any_instance_of(MultiCheck).to receive(:multi_check) + + subject.run_check(MultiCheck) + end + + it 'does not execute #check method' do + expect_any_instance_of(MultiCheck).not_to receive(:check) + + subject.run_check(MultiCheck) + end + + context 'when check implements #skip?' do + it 'executes #skip? method' do + expect_any_instance_of(SkipMultiCheck).to receive(:skip?).and_call_original + + subject.run_check(SkipMultiCheck) + end + end + end + end +end diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb new file mode 100644 index 00000000000..23d9beddb08 --- /dev/null +++ b/spec/lib/system_check_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck, lib: true do + class SimpleCheck < SystemCheck::BaseCheck + def check? + true + end + end + + class OtherCheck < SystemCheck::BaseCheck + def check? + false + end + end + + before do + silence_output + end + + describe '.run' do + subject { SystemCheck } + + it 'detects execution of SimpleCheck' do + is_expected.to execute_check(SimpleCheck) + + subject.run('Test', [SimpleCheck]) + end + + it 'detects exclusion of OtherCheck in execution' do + is_expected.not_to execute_check(OtherCheck) + + subject.run('Test', [SimpleCheck]) + end + end +end diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb new file mode 100644 index 00000000000..50f4bbda001 --- /dev/null +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -0,0 +1,117 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb') + +describe MigrateOldArtifacts do + let(:migration) { described_class.new } + let!(:directory) { Dir.mktmpdir } + + before do + allow(Gitlab.config.artifacts).to receive(:path).and_return(directory) + end + + after do + FileUtils.remove_entry_secure(directory) + end + + context 'with migratable data' do + let(:project1) { create(:empty_project, ci_id: 2) } + let(:project2) { create(:empty_project, ci_id: 3) } + let(:project3) { create(:empty_project) } + + let(:pipeline1) { create(:ci_empty_pipeline, project: project1) } + let(:pipeline2) { create(:ci_empty_pipeline, project: project2) } + let(:pipeline3) { create(:ci_empty_pipeline, project: project3) } + + let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) } + let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) } + let!(:build2) { create(:ci_build, :artifacts, pipeline: pipeline2) } + let!(:build3) { create(:ci_build, :artifacts, pipeline: pipeline3) } + + before do + store_artifacts_in_legacy_path(build_with_legacy_artifacts) + end + + it "legacy artifacts are not accessible" do + expect(build_with_legacy_artifacts.artifacts?).to be_falsey + end + + it "legacy artifacts are set" do + expect(build_with_legacy_artifacts.artifacts_file_identifier).not_to be_nil + end + + describe '#min_id' do + subject { migration.send(:min_id) } + + it 'returns the newest build for which ci_id is not defined' do + is_expected.to eq(build3.id) + end + end + + describe '#builds_with_artifacts' do + subject { migration.send(:builds_with_artifacts).map(&:id) } + + it 'returns a list of builds that has artifacts and could be migrated' do + is_expected.to contain_exactly(build_with_legacy_artifacts.id, build2.id) + end + end + + describe '#up' do + context 'when migrating artifacts' do + before do + migration.up + end + + it 'all files do have artifacts' do + Ci::Build.with_artifacts do |build| + expect(build).to have_artifacts + end + end + + it 'artifacts are no longer present on legacy path' do + expect(File.exist?(legacy_path(build_with_legacy_artifacts))).to eq(false) + end + end + + context 'when there are aritfacts in old and new directory' do + before do + store_artifacts_in_legacy_path(build2) + + migration.up + end + + it 'does not move old files' do + expect(File.exist?(legacy_path(build2))).to eq(true) + end + end + end + + private + + def store_artifacts_in_legacy_path(build) + FileUtils.mkdir_p(legacy_path(build)) + + FileUtils.copy( + Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + File.join(legacy_path(build), "ci_build_artifacts.zip")) + + FileUtils.copy( + Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + File.join(legacy_path(build), "ci_build_artifacts_metadata.gz")) + + build.update_columns( + artifacts_file: 'ci_build_artifacts.zip', + artifacts_metadata: 'ci_build_artifacts_metadata.gz') + + build.reload + end + + def legacy_path(build) + File.join(directory, + build.created_at.utc.strftime('%Y_%m'), + build.project.ci_id.to_s, + build.id.to_s) + end + end +end diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb index 92fbf64a6b7..d56379eb59d 100644 --- a/spec/models/blob_viewer/base_spec.rb +++ b/spec/models/blob_viewer/base_spec.rb @@ -11,8 +11,8 @@ describe BlobViewer::Base, model: true do self.extensions = %w(pdf) self.binary = true - self.overridable_max_size = 1.megabyte - self.max_size = 5.megabytes + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes end end @@ -69,77 +69,49 @@ describe BlobViewer::Base, model: true do end end - describe '#exceeds_overridable_max_size?' do - context 'when the blob size is larger than the overridable max size' do + describe '#collapsed?' do + context 'when the blob size is larger than the collapse limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns true' do - expect(viewer.exceeds_overridable_max_size?).to be_truthy + expect(viewer.collapsed?).to be_truthy end end - context 'when the blob size is smaller than the overridable max size' do + context 'when the blob size is smaller than the collapse limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } it 'returns false' do - expect(viewer.exceeds_overridable_max_size?).to be_falsey + expect(viewer.collapsed?).to be_falsey end end end - describe '#exceeds_max_size?' do - context 'when the blob size is larger than the max size' do + describe '#too_large?' do + context 'when the blob size is larger than the size limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } it 'returns true' do - expect(viewer.exceeds_max_size?).to be_truthy + expect(viewer.too_large?).to be_truthy end end - context 'when the blob size is smaller than the max size' do + context 'when the blob size is smaller than the size limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns false' do - expect(viewer.exceeds_max_size?).to be_falsey - end - end - end - - describe '#can_override_max_size?' do - context 'when the blob size is larger than the overridable max size' do - context 'when the blob size is larger than the max size' do - let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } - - it 'returns false' do - expect(viewer.can_override_max_size?).to be_falsey - end - end - - context 'when the blob size is smaller than the max size' do - let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } - - it 'returns true' do - expect(viewer.can_override_max_size?).to be_truthy - end - end - end - - context 'when the blob size is smaller than the overridable max size' do - let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } - - it 'returns false' do - expect(viewer.can_override_max_size?).to be_falsey + expect(viewer.too_large?).to be_falsey end end end describe '#render_error' do - context 'when the max size is overridden' do + context 'when expanded' do before do - viewer.override_max_size = true + viewer.expanded = true end - context 'when the blob size is larger than the max size' do + context 'when the blob size is larger than the size limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } it 'returns :too_large' do @@ -147,7 +119,7 @@ describe BlobViewer::Base, model: true do end end - context 'when the blob size is smaller than the max size' do + context 'when the blob size is smaller than the size limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns nil' do @@ -156,16 +128,16 @@ describe BlobViewer::Base, model: true do end end - context 'when the max size is not overridden' do - context 'when the blob size is larger than the overridable max size' do + context 'when not expanded' do + context 'when the blob size is larger than the collapse limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } - it 'returns :too_large' do - expect(viewer.render_error).to eq(:too_large) + it 'returns :collapsed' do + expect(viewer.render_error).to eq(:collapsed) end end - context 'when the blob size is smaller than the overridable max size' do + context 'when the blob size is smaller than the collapse limit' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } it 'returns nil' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e971b4bc3f9..e2406290c6c 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1215,16 +1215,49 @@ describe Ci::Build, :models do it { is_expected.to include(tag_variable) } end - context 'when secure variable is defined' do - let(:secure_variable) do + context 'when secret variable is defined' do + let(:secret_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + create(:ci_variable, + secret_variable.slice(:key, :value).merge(project: project)) end - it { is_expected.to include(secure_variable) } + it { is_expected.to include(secret_variable) } + end + + context 'when protected variable is defined' do + let(:protected_variable) do + { key: 'PROTECTED_KEY', value: 'protected_value', public: false } + end + + before do + create(:ci_variable, + :protected, + protected_variable.slice(:key, :value).merge(project: project)) + end + + context 'when the branch is protected' do + before do + create(:protected_branch, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } + end + + context 'when the tag is protected' do + before do + create(:protected_tag, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } + end + + context 'when the ref is not protected' do + it { is_expected.not_to include(protected_variable) } + end end context 'when build is for triggers' do @@ -1346,15 +1379,30 @@ describe Ci::Build, :models do end context 'returns variables in valid order' do + let(:build_pre_var) { { key: 'build', value: 'value' } } + let(:project_pre_var) { { key: 'project', value: 'value' } } + let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } } + let(:build_yaml_var) { { key: 'yaml', value: 'value' } } + before do - allow(build).to receive(:predefined_variables) { ['predefined'] } - allow(project).to receive(:predefined_variables) { ['project'] } - allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } - allow(build).to receive(:yaml_variables) { ['yaml'] } - allow(project).to receive(:secret_variables) { ['secret'] } + allow(build).to receive(:predefined_variables) { [build_pre_var] } + allow(project).to receive(:predefined_variables) { [project_pre_var] } + allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] } + allow(build).to receive(:yaml_variables) { [build_yaml_var] } + + allow(project).to receive(:secret_variables_for).with(build.ref) do + [create(:ci_variable, key: 'secret', value: 'value')] + end end - it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + it do + is_expected.to eq( + [build_pre_var, + project_pre_var, + pipeline_pre_var, + build_yaml_var, + { key: 'secret', value: 'value', public: false }]) + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c8023dc13b1..ae1b01b76ab 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -21,13 +21,35 @@ describe Ci::Pipeline, models: true do it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } - it { is_expected.to validate_presence_of :sha } - it { is_expected.to validate_presence_of :status } + it { is_expected.to validate_presence_of(:sha) } + it { is_expected.to validate_presence_of(:status) } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + describe '#source' do + context 'when creating new pipeline' do + let(:pipeline) do + build(:ci_empty_pipeline, status: :created, project: project, source: nil) + end + + it "prevents from creating an object" do + expect(pipeline).not_to be_valid + end + end + + context 'when updating existing pipeline' do + before do + pipeline.update_attribute(:source, nil) + end + + it "object is valid" do + expect(pipeline).to be_valid + end + end + end + describe '#block' do it 'changes pipeline status to manual' do expect(pipeline.block).to be true diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index fe8c52d5353..077b10227d7 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -12,11 +12,33 @@ describe Ci::Variable, models: true do it { is_expected.not_to allow_value('foo bar').for(:key) } it { is_expected.not_to allow_value('foo/bar').for(:key) } - before :each do - subject.value = secret_value + describe '.unprotected' do + subject { described_class.unprotected } + + context 'when variable is protected' do + before do + create(:ci_variable, :protected) + end + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'when variable is not protected' do + let(:variable) { create(:ci_variable, protected: false) } + + it 'returns the variable' do + is_expected.to contain_exactly(variable) + end + end end describe '#value' do + before do + subject.value = secret_value + end + it 'stores the encrypted value' do expect(subject.encrypted_value).not_to be_nil end @@ -36,4 +58,11 @@ describe Ci::Variable, models: true do to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') end end + + describe '#to_runner_variable' do + it 'returns a hash for the runner' do + expect(subject.to_runner_variable) + .to eq(key: subject.key, value: subject.value, public: false) + end + end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 4bda7d4314a..6f0d2db23c7 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -16,6 +16,19 @@ describe Deployment, models: true do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } + describe 'after_create callbacks' do + let(:environment) { create(:environment) } + let(:store) { Gitlab::EtagCaching::Store.new } + + it 'invalidates the environment etag cache' do + old_value = store.get(environment.etag_cache_key) + + create(:deployment, environment: environment) + + expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + end + end + describe '#includes_commit?' do let(:project) { create(:project, :repository) } let(:environment) { create(:environment, project: project) } diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 96f075d4f7d..297c2108dc2 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -160,12 +160,6 @@ describe DiffNote, models: true do context "when noteable is a commit" do let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) } - it "doesn't use the DiffPositionUpdateService" do - expect(Notes::DiffPositionUpdateService).not_to receive(:new) - - diff_note - end - it "doesn't update the position" do diff_note @@ -178,12 +172,6 @@ describe DiffNote, models: true do let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } context "when the note is active" do - it "doesn't use the DiffPositionUpdateService" do - expect(Notes::DiffPositionUpdateService).not_to receive(:new) - - diff_note - end - it "doesn't update the position" do diff_note @@ -197,18 +185,11 @@ describe DiffNote, models: true do allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) end - it "uses the DiffPositionUpdateService" do - service = instance_double("Notes::DiffPositionUpdateService") - expect(Notes::DiffPositionUpdateService).to receive(:new).with( - project, - nil, - old_diff_refs: position.diff_refs, - new_diff_refs: commit.diff_refs, - paths: [path] - ).and_return(service) - expect(service).to receive(:execute) - + it "updates the position" do diff_note + + expect(diff_note.original_position).to eq(position) + expect(diff_note.position).not_to eq(position) end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 12519de8636..fe69c8e351d 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment, models: true do - let(:project) { create(:empty_project) } + set(:project) { create(:empty_project) } subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } @@ -34,6 +34,26 @@ describe Environment, models: true do end end + describe 'state machine' do + it 'invalidates the cache after a change' do + expect(environment).to receive(:expire_etag_cache) + + environment.stop + end + end + + describe '#expire_etag_cache' do + let(:store) { Gitlab::EtagCaching::Store.new } + + it 'changes the cached value' do + old_value = store.get(environment.etag_cache_key) + + environment.stop + + expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") @@ -227,7 +247,10 @@ describe Environment, models: true do context 'when user is allowed to stop environment' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end context 'when action did not yet finish' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 712470d6bf5..060754fab63 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -238,10 +238,10 @@ describe MergeRequest, models: true do end context 'when there are no MR diffs' do - it 'delegates to the compare object, setting no_collapse: true' do + it 'delegates to the compare object, setting expanded: true' do merge_request.compare = double(:compare) - expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true)) + expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true)) merge_request.diffs(options) end @@ -1178,7 +1178,7 @@ describe MergeRequest, models: true do end describe "#reload_diff" do - let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) } + let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion } let(:commit) { subject.project.commit(sample_commit.id) } @@ -1197,7 +1197,7 @@ describe MergeRequest, models: true do subject.reload_diff end - it "updates diff note positions" do + it "updates diff discussion positions" do old_diff_refs = subject.diff_refs # Update merge_request_diff so that #diff_refs will return commit.diff_refs @@ -1211,15 +1211,15 @@ describe MergeRequest, models: true do subject.merge_request_diff(true) end - expect(Notes::DiffPositionUpdateService).to receive(:new).with( + expect(Discussions::UpdateDiffPositionService).to receive(:new).with( subject.project, subject.author, old_diff_refs: old_diff_refs, new_diff_refs: commit.diff_refs, - paths: note.position.paths + paths: discussion.position.paths ).and_call_original - expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) + expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original expect_any_instance_of(DiffNote).to receive(:save).once subject.reload_diff(subject.author) diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 1920b5bf42b..0ee050196e4 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -69,41 +69,6 @@ describe JiraService, models: true do end end - describe '#can_test?' do - let(:jira_service) { described_class.new } - - it 'returns false if username is blank' do - allow(jira_service).to receive_messages( - url: 'http://jira.example.com', - username: '', - password: '12345678' - ) - - expect(jira_service.can_test?).to be_falsy - end - - it 'returns false if password is blank' do - allow(jira_service).to receive_messages( - url: 'http://jira.example.com', - username: 'tester', - password: '' - ) - - expect(jira_service.can_test?).to be_falsy - end - - it 'returns true if password and username are present' do - jira_service = described_class.new - allow(jira_service).to receive_messages( - url: 'http://jira.example.com', - username: 'tester', - password: '12345678' - ) - - expect(jira_service.can_test?).to be_truthy - end - end - describe '#close_issue' do let(:custom_base_url) { 'http://custom_url' } let(:user) { create(:user) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2ef3d5654d5..86ab2550bfb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -948,6 +948,20 @@ describe Project, models: true do end end + describe '.starred_by' do + it 'returns only projects starred by the given user' do + user1 = create(:user) + user2 = create(:user) + project1 = create(:empty_project) + project2 = create(:empty_project) + create(:empty_project) + user1.toggle_star(project1) + user2.toggle_star(project2) + + expect(Project.starred_by(user1)).to contain_exactly(project1) + end + end + describe '.visible_to_user' do let!(:project) { create(:empty_project, :private) } let!(:user) { create(:user) } @@ -1735,6 +1749,90 @@ describe Project, models: true do end end + describe '#secret_variables_for' do + let(:project) { create(:empty_project) } + + let!(:secret_variable) do + create(:ci_variable, value: 'secret', project: project) + end + + let!(:protected_variable) do + create(:ci_variable, :protected, value: 'protected', project: project) + end + + subject { project.secret_variables_for('ref') } + + shared_examples 'ref is protected' do + it 'contains all the variables' do + is_expected.to contain_exactly(secret_variable, protected_variable) + end + end + + context 'when the ref is not protected' do + before do + stub_application_setting( + default_branch_protection: Gitlab::Access::PROTECTION_NONE) + end + + it 'contains only the secret variables' do + is_expected.to contain_exactly(secret_variable) + end + end + + context 'when the ref is a protected branch' do + before do + create(:protected_branch, name: 'ref', project: project) + end + + it_behaves_like 'ref is protected' + end + + context 'when the ref is a protected tag' do + before do + create(:protected_tag, name: 'ref', project: project) + end + + it_behaves_like 'ref is protected' + end + end + + describe '#protected_for?' do + let(:project) { create(:empty_project) } + + subject { project.protected_for?('ref') } + + context 'when the ref is not protected' do + before do + stub_application_setting( + default_branch_protection: Gitlab::Access::PROTECTION_NONE) + end + + it 'returns false' do + is_expected.to be_falsey + end + end + + context 'when the ref is a protected branch' do + before do + create(:protected_branch, name: 'ref', project: project) + end + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when the ref is a protected tag' do + before do + create(:protected_tag, name: 'ref', project: project) + end + + it 'returns true' do + is_expected.to be_truthy + end + end + end + describe '#update_project_statistics' do let(:project) { create(:empty_project) } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index fb2d5f60009..362565506e5 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -327,69 +327,114 @@ describe ProjectTeam, models: true do end end - shared_examples_for "#max_member_access_for_users" do |enable_request_store| - describe "#max_member_access_for_users" do + shared_examples 'max member access for users' do + let(:project) { create(:project) } + let(:group) { create(:group) } + let(:second_group) { create(:group) } + + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + + let(:promoted_guest) { create(:user) } + + let(:group_developer) { create(:user) } + let(:second_developer) { create(:user) } + + let(:user_without_access) { create(:user) } + let(:second_user_without_access) { create(:user) } + + let(:users) do + [master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id) + end + + let(:expected) do + { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER, + user_without_access.id => Gitlab::Access::NO_ACCESS + } + end + + before do + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(promoted_guest) + project.add_guest(guest) + + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER + ) + + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) + + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER + ) + + second_group.add_master(second_developer) + end + + it 'returns correct roles for different users' do + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + end + end + + describe '#max_member_access_for_user_ids' do + context 'with RequestStore enabled' do before do - RequestStore.begin! if enable_request_store + RequestStore.begin! end after do - if enable_request_store - RequestStore.end! - RequestStore.clear! - end + RequestStore.end! + RequestStore.clear! end - it 'returns correct roles for different users' do - master = create(:user) - reporter = create(:user) - promoted_guest = create(:user) - guest = create(:user) - project = create(:empty_project) + include_examples 'max member access for users' - project.add_master(master) - project.add_reporter(reporter) - project.add_guest(promoted_guest) - project.add_guest(guest) + def access_levels(users) + project.team.max_member_access_for_user_ids(users) + end + + it 'does not perform extra queries when asked for users who have already been found' do + access_levels(users) + + expect { access_levels(users) }.not_to exceed_query_limit(0) - group = create(:group) - group_developer = create(:user) - second_developer = create(:user) - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::DEVELOPER) - - group.add_master(promoted_guest) - group.add_developer(group_developer) - group.add_developer(second_developer) - - second_group = create(:group) - project.project_group_links.create( - group: second_group, - group_access: Gitlab::Access::MASTER) - second_group.add_master(second_developer) - - users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) - - expected = { - master.id => Gitlab::Access::MASTER, - reporter.id => Gitlab::Access::REPORTER, - promoted_guest.id => Gitlab::Access::DEVELOPER, - guest.id => Gitlab::Access::GUEST, - group_developer.id => Gitlab::Access::DEVELOPER, - second_developer.id => Gitlab::Access::MASTER - } - - expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + expect(access_levels(users)).to eq(expected) end - end - end - describe '#max_member_access_for_users with RequestStore' do - it_behaves_like "#max_member_access_for_users", true - end + it 'only requests the extra users when uncached users are passed' do + new_user = create(:user) + second_new_user = create(:user) + all_users = users + [new_user.id, second_new_user.id] + + expected_all = expected.merge(new_user.id => Gitlab::Access::NO_ACCESS, + second_new_user.id => Gitlab::Access::NO_ACCESS) + + access_levels(users) - describe '#max_member_access_for_users without RequestStore' do - it_behaves_like "#max_member_access_for_users", false + queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) } + + expect(queries.count).to eq(1) + expect(queries.log_message).to match(/\W#{new_user.id}\W/) + expect(queries.log_message).to match(/\W#{second_new_user.id}\W/) + expect(queries.log_message).not_to match(/\W#{promoted_guest.id}\W/) + expect(access_levels(all_users)).to eq(expected_all) + end + end + + context 'with RequestStore disabled' do + include_examples 'max member access for users' + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9edf34b05ad..1c3541da44f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -22,7 +22,7 @@ describe User, models: true do it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } - it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) } + it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } @@ -1496,25 +1496,6 @@ describe User, models: true do end end - describe '#viewable_starred_projects' do - let(:user) { create(:user) } - let(:public_project) { create(:empty_project, :public) } - let(:private_project) { create(:empty_project, :private) } - let(:private_viewable_project) { create(:empty_project, :private) } - - before do - private_viewable_project.team << [user, Gitlab::Access::MASTER] - - [public_project, private_project, private_viewable_project].each do |project| - user.toggle_star(project) - end - end - - it 'returns only starred projects the user can view' do - expect(user.viewable_starred_projects).not_to include(private_project) - end - end - describe '#projects_with_reporter_access_limited_to' do let(:project1) { create(:empty_project) } let(:project2) { create(:empty_project) } diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 1c163cee152..6b637a03b6f 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -16,8 +16,8 @@ describe API::CommitStatuses do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } - let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } + let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master') } + let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop') } context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index b84361d3abd..b0c265b6453 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -485,7 +485,7 @@ describe API::Commits do end it "returns status for CI" do - pipeline = project.ensure_pipeline('master', project.repository.commit.sha) + pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) pipeline.update(status: 'success') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -495,7 +495,7 @@ describe API::Commits do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline('master', project.repository.commit.sha) + project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb new file mode 100644 index 00000000000..f169e6661d1 --- /dev/null +++ b/spec/requests/api/features_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe API::Features do + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /features' do + let(:expected_features) do + [ + { + 'name' => 'feature_1', + 'state' => 'on', + 'gates' => [{ 'key' => 'boolean', 'value' => true }] + }, + { + 'name' => 'feature_2', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }] + } + ] + end + + before do + Feature.get('feature_1').enable + Feature.get('feature_2').disable + end + + it 'returns a 401 for anonymous users' do + get api('/features') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/features', user) + + expect(response).to have_http_status(403) + end + + it 'returns the feature list for admins' do + get api('/features', admin) + + expect(response).to have_http_status(200) + expect(json_response).to match_array(expected_features) + end + end + + describe 'POST /feature' do + let(:feature_name) { 'my_feature' } + it 'returns a 401 for anonymous users' do + post api("/features/#{feature_name}") + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + post api("/features/#{feature_name}", user) + + expect(response).to have_http_status(403) + end + + it 'creates an enabled feature if passed true' do + post api("/features/#{feature_name}", admin), value: 'true' + + expect(response).to have_http_status(201) + expect(Feature.get(feature_name)).to be_enabled + end + + it 'creates a feature with the given percentage if passed an integer' do + post api("/features/#{feature_name}", admin), value: '50' + + expect(response).to have_http_status(201) + expect(Feature.get(feature_name).percentage_of_time_value).to be(50) + end + + context 'when the feature exists' do + let(:feature) { Feature.get(feature_name) } + + before do + feature.disable # This also persists the feature on the DB + end + + it 'enables the feature if passed true' do + post api("/features/#{feature_name}", admin), value: 'true' + + expect(response).to have_http_status(201) + expect(feature).to be_enabled + end + + context 'with a pre-existing percentage value' do + before do + feature.enable_percentage_of_time(50) + end + + it 'updates the percentage of time if passed an integer' do + post api("/features/#{feature_name}", admin), value: '30' + + expect(response).to have_http_status(201) + expect(Feature.get(feature_name).percentage_of_time_value).to be(30) + end + end + end + end +end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb new file mode 100644 index 00000000000..85d11deb26f --- /dev/null +++ b/spec/requests/api/pipeline_schedules_spec.rb @@ -0,0 +1,297 @@ +require 'spec_helper' + +describe API::PipelineSchedules do + set(:developer) { create(:user) } + set(:user) { create(:user) } + set(:project) { create(:project) } + + before do + project.add_developer(developer) + end + + describe 'GET /projects/:id/pipeline_schedules' do + context 'authenticated user with valid permissions' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } + + before do + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + + it 'returns list of pipeline_schedules' do + get api("/projects/#{project.id}/pipeline_schedules", developer) + + expect(response).to have_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('pipeline_schedules') + end + + it 'avoids N + 1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/pipeline_schedules", developer) + end.count + + create_list(:ci_pipeline_schedule, 10, project: project) + .each do |pipeline_schedule| + create(:user).tap do |user| + project.add_developer(user) + pipeline_schedule.update_attributes(owner: user) + end + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + + expect do + get api("/projects/#{project.id}/pipeline_schedules", developer) + end.not_to exceed_query_limit(control_count) + end + + %w[active inactive].each do |target| + context "when scope is #{target}" do + before do + create(:ci_pipeline_schedule, project: project, active: active?(target)) + end + + it 'returns matched pipeline schedules' do + get api("/projects/#{project.id}/pipeline_schedules", developer), scope: target + + expect(json_response.map{ |r| r['active'] }).to all(eq(active?(target))) + end + end + + def active?(str) + (str == 'active') ? true : false + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules", user) + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules") + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } + + before do + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + + context 'authenticated user with valid permissions' do + it 'returns pipeline_schedule details' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule') + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do + get api("/projects/#{project.id}/pipeline_schedules/-5", developer) + + expect(response).to have_http_status(:not_found) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules' do + let(:params) { attributes_for(:ci_pipeline_schedule) } + + context 'authenticated user with valid permissions' do + context 'with required parameters' do + it 'creates pipeline_schedule' do + expect do + post api("/projects/#{project.id}/pipeline_schedules", developer), + params + end.to change { project.pipeline_schedules.count }.by(1) + + expect(response).to have_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule') + expect(json_response['description']).to eq(params[:description]) + expect(json_response['ref']).to eq(params[:ref]) + expect(json_response['cron']).to eq(params[:cron]) + expect(json_response['cron_timezone']).to eq(params[:cron_timezone]) + expect(json_response['owner']['id']).to eq(developer.id) + end + end + + context 'without required parameters' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", developer) + + expect(response).to have_http_status(:bad_request) + end + end + + context 'when cron has validation error' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", developer), + params.merge('cron' => 'invalid-cron') + + expect(response).to have_http_status(:bad_request) + expect(json_response['message']).to have_key('cron') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", user), params + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules"), params + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + it 'updates cron' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), + cron: '1 2 3 4 *' + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule') + expect(json_response['cron']).to eq('1 2 3 4 *') + end + + context 'when cron has validation error' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), + cron: 'invalid-cron' + + expect(response).to have_http_status(:bad_request) + expect(json_response['message']).to have_key('cron') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do + let(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + it 'updates owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer) + + expect(response).to have_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule') + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user) + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership") + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:master) { create(:user) } + + let!(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + before do + project.add_master(master) + end + + context 'authenticated user with valid permissions' do + it 'deletes pipeline_schedule' do + expect do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master) + end.to change { project.pipeline_schedules.count }.by(-1) + + expect(response).to have_http_status(:accepted) + expect(response).to match_response_schema('pipeline_schedule') + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/-5", master) + + expect(response).to have_http_status(:not_found) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not delete pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) + + expect(response).to have_http_status(:forbidden) + end + end + + context 'unauthenticated user' do + it 'does not delete pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f95a287a184..40bfc0c636b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -316,15 +316,15 @@ describe API::Projects do expect(project.path).to eq('foo_project') end - it 'creates new project name and path and returns 201' do - expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. + it 'creates new project with name and path and returns 201' do + expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' }. to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first expect(project.name).to eq('Foo Project') - expect(project.path).to eq('foo-Project') + expect(project.path).to eq('path-project-Foo') end it 'creates last project before reaching project limit' do @@ -390,6 +390,14 @@ describe API::Projects do expect(json_response['visibility']).to eq('private') end + it 'sets tag list to a project' do + project = attributes_for(:project, tag_list: %w[tagFirst tagSecond]) + + post api('/projects', user), project + + expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond]) + end + it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api('/projects', user), project @@ -462,9 +470,25 @@ describe API::Projects do before { project } before { admin } - it 'creates new project without path and return 201' do - expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) + it 'creates new project without path but with name and return 201' do + expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-project') + end + + it 'creates new project with name and path and returns 201' do + expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' }. + to change { Project.count }.by(1) expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('path-project-Foo') end it 'responds with 400 on failure and not project' do diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 386f60065ad..4a4a5dc5c7c 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -386,7 +386,7 @@ describe API::V3::Commits do end it "returns status for CI" do - pipeline = project.ensure_pipeline('master', project.repository.commit.sha) + pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) pipeline.update(status: 'success') get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -396,7 +396,7 @@ describe API::V3::Commits do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline('master', project.repository.commit.sha) + project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb index b61b2b618a6..94f4d93a8dc 100644 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ b/spec/requests/api/v3/deploy_keys_spec.rb @@ -105,6 +105,15 @@ describe API::V3::DeployKeys do expect(response).to have_http_status(201) end + + it 'accepts can_push parameter' do + key_attrs = attributes_for :write_access_key + + post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs + + expect(response).to have_http_status(201) + expect(json_response['can_push']).to eq(true) + end end describe "DELETE /projects/:id/#{path}/:key_id" do diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 63d6d3001ac..83673864fe7 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -42,6 +42,7 @@ describe API::Variables do expect(response).to have_http_status(200) expect(json_response['value']).to eq(variable.value) + expect(json_response['protected']).to eq(variable.protected?) end it 'responds with 404 Not Found if requesting non-existing variable' do @@ -72,12 +73,13 @@ describe API::Variables do context 'authorized user with proper permissions' do it 'creates variable' do expect do - post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' + post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true end.to change{project.variables.count}.by(1) expect(response).to have_http_status(201) expect(json_response['key']).to eq('TEST_VARIABLE_2') expect(json_response['value']).to eq('VALUE_2') + expect(json_response['protected']).to be_truthy end it 'does not allow to duplicate variable key' do @@ -112,13 +114,14 @@ describe API::Variables do initial_variable = project.variables.first value_before = initial_variable.value - put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP' + put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true updated_variable = project.variables.first expect(response).to have_http_status(200) expect(value_before).to eq(variable.value) expect(updated_variable.value).to eq('VALUE_1_UP') + expect(updated_variable).to be_protected end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/rubocop/cop/activerecord_serialize_spec.rb b/spec/rubocop/cop/activerecord_serialize_spec.rb new file mode 100644 index 00000000000..a303b16d264 --- /dev/null +++ b/spec/rubocop/cop/activerecord_serialize_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/activerecord_serialize' + +describe RuboCop::Cop::ActiverecordSerialize do + include CopHelper + + subject(:cop) { described_class.new } + + context 'inside the app/models directory' do + it 'registers an offense when serialize is used' do + allow(cop).to receive(:in_models?).and_return(true) + + inspect_source(cop, 'serialize :foo') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + + context 'outside the app/models directory' do + it 'does nothing' do + allow(cop).to receive(:in_models?).and_return(false) + + inspect_source(cop, 'serialize :foo') + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb index 059deba5416..15720d86583 100644 --- a/spec/serializers/build_action_entity_spec.rb +++ b/spec/serializers/build_action_entity_spec.rb @@ -1,26 +1,26 @@ require 'spec_helper' describe BuildActionEntity do - let(:build) { create(:ci_build, name: 'test_build') } + let(:job) { create(:ci_build, name: 'test_job') } let(:request) { double('request') } let(:entity) do - described_class.new(build, request: spy('request')) + described_class.new(job, request: spy('request')) end describe '#as_json' do subject { entity.as_json } - it 'contains original build name' do - expect(subject[:name]).to eq 'test_build' + it 'contains original job name' do + expect(subject[:name]).to eq 'test_job' end it 'contains path to the action play' do - expect(subject[:path]).to include "builds/#{build.id}/play" + expect(subject[:path]).to include "jobs/#{job.id}/play" end it 'contains whether it is playable' do - expect(subject[:playable]).to eq build.playable? + expect(subject[:playable]).to eq job.playable? end end end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb index 2fc60aa9de6..b4eef20d6a6 100644 --- a/spec/serializers/build_artifact_entity_spec.rb +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -1,22 +1,22 @@ require 'spec_helper' describe BuildArtifactEntity do - let(:build) { create(:ci_build, name: 'test:build') } + let(:job) { create(:ci_build, name: 'test:job') } let(:entity) do - described_class.new(build, request: double) + described_class.new(job, request: double) end describe '#as_json' do subject { entity.as_json } - it 'contains build name' do - expect(subject[:name]).to eq 'test:build' + it 'contains job name' do + expect(subject[:name]).to eq 'test:job' end it 'contains path to the artifacts' do expect(subject[:path]) - .to include "builds/#{build.id}/artifacts/download" + .to include "jobs/#{job.id}/artifacts/download" end end end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index b5eb84ae43b..6d5e1046e86 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe BuildEntity do let(:user) { create(:user) } let(:build) { create(:ci_build) } + let(:project) { build.project } let(:request) { double('request') } before do @@ -52,7 +53,10 @@ describe BuildEntity do context 'when user is allowed to trigger action' do before do - build.project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end it 'contains path to play action' do diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index d2482ac434b..88ec4ed2952 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -19,7 +19,7 @@ describe PipelineEntity do let(:pipeline) { create(:ci_empty_pipeline) } it 'contains required fields' do - expect(subject).to include :id, :user, :path, :coverage + expect(subject).to include :id, :user, :path, :coverage, :source expect(subject).to include :ref, :commit expect(subject).to include :updated_at, :created_at end @@ -36,7 +36,7 @@ describe PipelineEntity do it 'contains flags' do expect(subject).to include :flags expect(subject[:flags]) - .to include :latest, :triggered, :stuck, + .to include :latest, :stuck, :yaml_errors, :retryable, :cancelable end end diff --git a/spec/serializers/user_entity_spec.rb b/spec/serializers/user_entity_spec.rb index c5d11cbcf5e..cd778e49107 100644 --- a/spec/serializers/user_entity_spec.rb +++ b/spec/serializers/user_entity_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe UserEntity do + include Gitlab::Routing + let(:entity) { described_class.new(user) } let(:user) { create(:user) } subject { entity.as_json } @@ -20,4 +22,8 @@ describe UserEntity do it 'does not expose 2FA OTPs' do expect(subject).not_to include(/otp/) end + + it 'exposes user path' do + expect(subject[:path]).to eq user_path(user) + end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 030912b9f45..06fbd7bad90 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -9,13 +9,13 @@ describe Ci::CreatePipelineService, services: true do end describe '#execute' do - def execute_service(after: project.commit.id, message: 'Message', ref: 'refs/heads/master') + def execute_service(source: :push, after: project.commit.id, message: 'Message', ref: 'refs/heads/master') params = { ref: ref, before: '00000000', after: after, commits: [{ message: message }] } - described_class.new(project, user, params).execute + described_class.new(project, user, params).execute(source) end context 'valid params' do @@ -30,6 +30,7 @@ describe Ci::CreatePipelineService, services: true do it 'creates a pipeline' do expect(pipeline).to be_kind_of(Ci::Pipeline) expect(pipeline).to be_valid + expect(pipeline).to be_push expect(pipeline).to eq(project.pipelines.last) expect(pipeline).to have_attributes(user: user) expect(pipeline).to have_attributes(status: 'pending') @@ -71,10 +72,11 @@ describe Ci::CreatePipelineService, services: true do end end - context 'when merge request head commit sha does not match pipeline sha' do + context 'when the pipeline is not the latest for the branch' do it 'does not update merge request head pipeline' do merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) - allow_any_instance_of(MergeRequestDiff).to receive(:head_commit).and_return(double(id: 1234)) + + allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false) pipeline diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 5a20102872a..f2956262f4b 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -16,6 +16,7 @@ describe Ci::CreateTriggerRequestService, services: true do context 'without owner' do it { expect(subject).to be_kind_of(Ci::TriggerRequest) } it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline).to be_trigger } it { expect(subject.builds.first).to be_kind_of(Ci::Build) } end @@ -25,6 +26,7 @@ describe Ci::CreateTriggerRequestService, services: true do it { expect(subject).to be_kind_of(Ci::TriggerRequest) } it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline).to be_trigger } it { expect(subject.pipeline.user).to eq(owner) } it { expect(subject.builds.first).to be_kind_of(Ci::Build) } it { expect(subject.builds.first.user).to eq(owner) } diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb index d6f9fa42045..ea211de1f82 100644 --- a/spec/services/ci/play_build_service_spec.rb +++ b/spec/services/ci/play_build_service_spec.rb @@ -13,8 +13,11 @@ describe Ci::PlayBuildService, '#execute', :services do context 'when project does not have repository yet' do let(:project) { create(:empty_project) } - it 'allows user with master role to play build' do - project.add_master(user) + it 'allows user to play build if protected branch rules are met' do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) service.execute(build) @@ -45,7 +48,10 @@ describe Ci::PlayBuildService, '#execute', :services do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'enqueues the build' do @@ -64,7 +70,10 @@ describe Ci::PlayBuildService, '#execute', :services do let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) } before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'duplicates the build' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index fc5de5d069a..1557cb3c938 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -333,10 +333,11 @@ describe Ci::ProcessPipelineService, '#execute', :services do context 'when pipeline is promoted sequentially up to the end' do before do - # We are using create(:empty_project), and users has to be master in - # order to execute manual action when repository does not exist. + # Users need ability to merge into a branch in order to trigger + # protected manual actions. # - project.add_master(user) + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end it 'properly processes entire pipeline' do diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index d941d56c0d8..3e860203063 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -6,9 +6,12 @@ describe Ci::RetryPipelineService, '#execute', :services do let(:pipeline) { create(:ci_pipeline, project: project) } let(:service) { described_class.new(project, user) } - context 'when user has ability to modify pipeline' do + context 'when user has full ability to modify pipeline' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) end context 'when there are already retried jobs present' do diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index 380c296fd3a..177e32e13bd 100644 --- a/spec/services/notes/diff_position_update_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Notes::DiffPositionUpdateService, services: true do +describe Discussions::UpdateDiffPositionService, services: true do let(:project) { create(:project, :repository) } let(:current_user) { project.owner } let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } @@ -138,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do # .. .. describe "#execute" do - let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } + let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion } let(:old_position) do Gitlab::Diff::Position.new( @@ -154,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do let(:line) { 16 } it "updates the position" do - subject.execute(note) + subject.execute(discussion) - expect(note.original_position).to eq(old_position) - expect(note.position).not_to eq(old_position) - expect(note.position.new_line).to eq(22) + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).not_to eq(old_position) + expect(discussion.position.new_line).to eq(22) end end @@ -166,27 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do let(:line) { 9 } it "doesn't update the position" do - subject.execute(note) + subject.execute(discussion) - expect(note.original_position).to eq(old_position) - expect(note.position).to eq(old_position) + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).to eq(old_position) end it 'sets the change position' do - subject.execute(note) + subject.execute(discussion) - change_position = note.change_position + change_position = discussion.change_position expect(change_position.start_sha).to eq(old_diff_refs.head_sha) expect(change_position.head_sha).to eq(new_diff_refs.head_sha) expect(change_position.old_line).to eq(9) expect(change_position.new_line).to be_nil end - it 'creates a system note' do + it 'creates a system discussion' do expect(SystemNoteService).to receive(:diff_discussion_outdated).with( - note.to_discussion, project, current_user, instance_of(Gitlab::Diff::Position)) + discussion, project, current_user, instance_of(Gitlab::Diff::Position)) - subject.execute(note) + subject.execute(discussion) end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 9f5a8beac16..bcd1fb64ab9 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -131,6 +131,19 @@ describe GitPushService, services: true do end end + describe "Pipelines" do + subject { execute_service(project, user, @oldrev, @newrev, @ref) } + + before do + stub_ci_pipeline_to_return_yaml_file + end + + it "creates a new pipeline" do + expect{ subject }.to change{ Ci::Pipeline.count } + expect(Ci::Pipeline.last).to be_push + end + end + describe "Push Event" do before do service = execute_service(project, user, @oldrev, @newrev, @ref ) diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index b73beb3f6fc..1fdcb420a8b 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -30,6 +30,20 @@ describe GitTagPushService, services: true do end end + describe "Pipelines" do + subject { service.execute } + + before do + stub_ci_pipeline_to_return_yaml_file + project.team << [user, :developer] + end + + it "creates a new pipeline" do + expect{ subject }.to change{ Ci::Pipeline.count } + expect(Ci::Pipeline.last).to be_push + end + end + describe "Git Tag Push Data" do subject { @push_data } let(:tag) { project.repository.find_tag(tag_name) } diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb new file mode 100644 index 00000000000..8c4ad8c7a3e --- /dev/null +++ b/spec/services/gravatar_service_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe GravatarService, service: true do + describe '#execute' do + let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' } + + before do + allow(Gitlab.config.gravatar).to receive(:plain_url).and_return(url) + end + + it 'replaces the placeholders' do + avatar_url = described_class.new.execute('user@example.com', 100, 2, username: 'user') + + expect(avatar_url).to include("hash=#{Digest::MD5.hexdigest('user@example.com')}") + expect(avatar_url).to include("size=200") + expect(avatar_url).to include("email=user%40example.com") + expect(avatar_url).to include("username=user") + end + end +end diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 19e8d5cc5f1..c77e6e9cd50 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -26,6 +26,10 @@ describe MergeRequests::Conflicts::ResolveService do describe '#execute' do let(:service) { described_class.new(merge_request) } + def blob_content(project, ref, path) + project.repository.blob_at(ref, path).data + end + context 'with section params' do let(:params) do { @@ -66,6 +70,35 @@ describe MergeRequests::Conflicts::ResolveService do end end + context 'when some files have trailing newlines' do + let!(:source_head) do + branch = 'conflict-resolvable' + path = 'files/ruby/popen.rb' + popen_content = blob_content(project, branch, path) + + project.repository.update_file( + user, + path, + popen_content.chomp("\n"), + message: 'Remove trailing newline from popen.rb', + branch_name: branch + ) + end + + before do + service.execute(user, params) + end + + it 'preserves trailing newlines from our side of the conflicts' do + head_sha = merge_request.source_branch_head.sha + popen_content = blob_content(project, head_sha, 'files/ruby/popen.rb') + regex_content = blob_content(project, head_sha, 'files/ruby/regex.rb') + + expect(popen_content).not_to end_with("\n") + expect(regex_content).to end_with("\n") + end + end + context 'when the source project is a fork and does not contain the HEAD of the target branch' do let!(:target_head) do project.repository.create_file( @@ -142,10 +175,13 @@ describe MergeRequests::Conflicts::ResolveService do end it 'sets the content to the content given' do - blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha, - 'files/ruby/popen.rb') + blob = blob_content( + merge_request.source_project, + merge_request.source_branch_head.sha, + 'files/ruby/popen.rb' + ) - expect(blob.data).to eq(popen_content) + expect(blob).to eq(popen_content) end end diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index de37a61e388..5409f67c091 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -147,16 +147,22 @@ describe Users::DestroyService, services: true do end context "migrating associated records" do + let!(:issue) { create(:issue, author: user) } + it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do - expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once + expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original service.execute(user) + + expect(issue.reload.author).to be_ghost end it 'does not run `MigrateToGhostUser` if hard_delete option is given' do expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute) service.execute(user, hard_delete: true) + + expect(Issue.exists?(issue.id)).to be_falsy end end end diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb index 5341ba3d261..054e28ae7b0 100644 --- a/spec/services/wiki_pages/create_service_spec.rb +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe WikiPages::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:opts) do { title: 'Title', @@ -10,27 +11,28 @@ describe WikiPages::CreateService, services: true do format: 'markdown' } end - let(:service) { described_class.new(project, user, opts) } + + subject(:service) { described_class.new(project, user, opts) } + + before do + project.add_developer(user) + end describe '#execute' do - context "valid params" do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end - - subject { service.execute } - - it 'creates a valid wiki page' do - is_expected.to be_valid - expect(subject.title).to eq(opts[:title]) - expect(subject.content).to eq(opts[:content]) - expect(subject.format).to eq(opts[:format].to_sym) - end - - it 'executes webhooks' do - expect(service).to have_received(:execute_hooks).once.with(subject, 'create') - end + it 'creates wiki page with valid attributes' do + page = service.execute + + expect(page).to be_valid + expect(page.title).to eq(opts[:title]) + expect(page.content).to eq(opts[:content]) + expect(page.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'create') + + service.execute end end end diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb index a4b9a390fe2..920be4d4c8a 100644 --- a/spec/services/wiki_pages/destroy_service_spec.rb +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -3,19 +3,20 @@ require 'spec_helper' describe WikiPages::DestroyService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:wiki_page) { create(:wiki_page) } - let(:service) { described_class.new(project, user) } + let(:page) { create(:wiki_page) } - describe '#execute' do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end + subject(:service) { described_class.new(project, user) } + before do + project.add_developer(user) + end + + describe '#execute' do it 'executes webhooks' do - service.execute(wiki_page) + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'delete') - expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete') + service.execute(page) end end end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb index 2bccca764d7..5e36ea4cf94 100644 --- a/spec/services/wiki_pages/update_service_spec.rb +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe WikiPages::UpdateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:wiki_page) { create(:wiki_page) } + let(:page) { create(:wiki_page) } + let(:opts) do { content: 'New content for wiki page', @@ -11,27 +12,28 @@ describe WikiPages::UpdateService, services: true do message: 'New wiki message' } end - let(:service) { described_class.new(project, user, opts) } + + subject(:service) { described_class.new(project, user, opts) } + + before do + project.add_developer(user) + end describe '#execute' do - context "valid params" do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end - - subject { service.execute(wiki_page) } - - it 'updates the wiki page' do - is_expected.to be_valid - expect(subject.content).to eq(opts[:content]) - expect(subject.format).to eq(opts[:format].to_sym) - expect(subject.message).to eq(opts[:message]) - end - - it 'executes webhooks' do - expect(service).to have_received(:execute_hooks).once.with(subject, 'update') - end + it 'updates the wiki page' do + updated_page = service.execute(page) + + expect(updated_page).to be_valid + expect(updated_page.message).to eq(opts[:message]) + expect(updated_page.content).to eq(opts[:content]) + expect(updated_page.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'update') + + service.execute(page) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4c2eba8fa46..994c7dcbb46 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,6 +26,9 @@ if ENV['CI'] && !ENV['NO_KNAPSACK'] Knapsack::Adapters::RSpecAdapter.bind end +# require rainbow gem String monkeypatch, so we can test SystemChecks +require 'rainbow/ext/string' + # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index 7aca902fc61..2bf159002a0 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -1,6 +1,7 @@ if Gitlab::GitalyClient.enabled? RSpec.configure do |config| - config.before(:each) do + config.before(:each) do |example| + next if example.metadata[:skip_gitaly_mock] allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) end end diff --git a/spec/support/matchers/execute_check.rb b/spec/support/matchers/execute_check.rb new file mode 100644 index 00000000000..7232fad52fb --- /dev/null +++ b/spec/support/matchers/execute_check.rb @@ -0,0 +1,23 @@ +RSpec::Matchers.define :execute_check do |expected| + match do |actual| + expect(actual).to eq(SystemCheck) + expect(actual).to receive(:run) do |*args| + expect(args[1]).to include(expected) + end + end + + match_when_negated do |actual| + expect(actual).to eq(SystemCheck) + expect(actual).to receive(:run) do |*args| + expect(args[1]).not_to include(expected) + end + end + + failure_message do |actual| + 'This matcher must be used with SystemCheck' unless actual == SystemCheck + end + + failure_message_when_negated do |actual| + 'This matcher must be used with SystemCheck' unless actual == SystemCheck + end +end diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb index 4a8158ed79b..5cb415111d2 100644 --- a/spec/support/rake_helpers.rb +++ b/spec/support/rake_helpers.rb @@ -7,4 +7,9 @@ module RakeHelpers def stub_warn_user_is_not_gitlab allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab) end + + def silence_output + allow($stdout).to receive(:puts) + allow($stdout).to receive(:print) + end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3c000feba5d..72b3b226c1e 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -40,7 +40,7 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '6d85bb6', + 'add-ipython-files' => '93ee732', 'add-pdf-file' => 'e774ebd' }.freeze diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb new file mode 100644 index 00000000000..24e2e3a9f0e --- /dev/null +++ b/spec/uploaders/artifact_uploader_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe ArtifactUploader do + let(:job) { create(:ci_build) } + let(:uploader) { described_class.new(job, :artifacts_file) } + let(:path) { Gitlab.config.artifacts.path } + + describe '.local_artifacts_store' do + subject { described_class.local_artifacts_store } + + it "delegate to artifacts path" do + expect(Gitlab.config.artifacts).to receive(:path) + + subject + end + end + + describe '.artifacts_upload_path' do + subject { described_class.artifacts_upload_path } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('tmp/uploads/') } + end + + describe '#store_dir' do + subject { uploader.store_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with("#{job.project_id}/#{job.id}") } + end + + describe '#cache_dir' do + subject { uploader.cache_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('tmp/cache') } + end +end diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb new file mode 100644 index 00000000000..78e9d9cf46c --- /dev/null +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' +require 'carrierwave/storage/fog' + +describe GitlabUploader do + let(:uploader_class) { Class.new(described_class) } + + subject { uploader_class.new } + + describe '#file_storage?' do + context 'when file storage is used' do + before do + uploader_class.storage(:file) + end + + it { is_expected.to be_file_storage } + end + + context 'when is remote storage' do + before do + uploader_class.storage(:fog) + end + + it { is_expected.not_to be_file_storage } + end + end + + describe '#file_cache_storage?' do + context 'when file storage is used' do + before do + uploader_class.cache_storage(:file) + end + + it { is_expected.to be_file_cache_storage } + end + + context 'when is remote storage' do + before do + uploader_class.cache_storage(:fog) + end + + it { is_expected.not_to be_file_cache_storage } + end + end + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb index c62450fb8e2..72323da2838 100644 --- a/spec/views/ci/status/_badge.html.haml_spec.rb +++ b/spec/views/ci/status/_badge.html.haml_spec.rb @@ -16,7 +16,7 @@ describe 'ci/status/_badge', :view do end it 'has link to build details page' do - details_path = namespace_project_build_path( + details_path = namespace_project_job_path( project.namespace, project, build) render_status(build) diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index c6b0ed8da3c..bbd7f98fa8d 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -10,8 +10,8 @@ describe 'projects/blob/_viewer.html.haml', :view do include BlobViewer::Rich self.partial_name = 'text' - self.overridable_max_size = 1.megabyte - self.max_size = 5.megabytes + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes self.load_async = true end end diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb index 751482cac42..1d58891036e 100644 --- a/spec/views/projects/builds/_build.html.haml_spec.rb +++ b/spec/views/projects/jobs/_build.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/ci/builds/_build' do +describe 'projects/ci/jobs/_build' do include Devise::Test::ControllerHelpers let(:project) { create(:project, :repository) } diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb index dc2ffc9dc47..dc2ffc9dc47 100644 --- a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb +++ b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 0f39df0f250..8f2822f5dc5 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/builds/show', :view do +describe 'projects/jobs/show', :view do let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } @@ -278,7 +278,7 @@ describe 'projects/builds/show', :view do it 'links to issues/new with the title and description filled in' do title = "Build Failed ##{build.id}" - build_url = namespace_project_build_url(project.namespace, project, build) + build_url = namespace_project_job_url(project.namespace, project, build) href = new_namespace_project_issue_path( project.namespace, project, diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index 9c650354d72..14ed8b7811e 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -23,7 +23,8 @@ describe PipelineScheduleWorker do context 'when there is a scheduled pipeline within next_run_at' do it 'creates a new pipeline' do - expect { subject }.to change { project.pipelines.count }.by(1) + expect{ subject }.to change { project.pipelines.count }.by(1) + expect(Ci::Pipeline.last).to be_schedule end it 'updates the next_run_at field' do |
