diff options
author | Chris Baumbauer <cab@cabnetworks.net> | 2018-11-02 08:39:25 -0700 |
---|---|---|
committer | Chris Baumbauer <cab@cabnetworks.net> | 2018-11-02 08:39:25 -0700 |
commit | dc078c241765cfea5f49409407b82db7296c132d (patch) | |
tree | deeabb4b302f896b60ffe8e27163a4dddf625c4b /spec | |
parent | 28a9bbceb7ab5ffe2305db02052d663b1e68ab8e (diff) | |
parent | a80ee886e4b4a41ace5fb796bf920c4e395bf4dd (diff) | |
download | gitlab-ce-dc078c241765cfea5f49409407b82db7296c132d.tar.gz |
Merge branch 'master' into triggermesh-phase1-knative
Diffstat (limited to 'spec')
216 files changed, 4596 insertions, 1777 deletions
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index be3fc832008..4e91068ab88 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -792,4 +792,30 @@ describe ApplicationController do end end end + + context 'control headers' do + controller(described_class) do + def index + render json: :ok + end + end + + context 'user not logged in' do + it 'sets the default headers' do + get :index + + expect(response.headers['Cache-Control']).to be_nil + end + end + + context 'user logged in' do + it 'sets the default headers' do + sign_in(user) + + get :index + + expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store' + end + end + end end diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index c365988a100..98946e4287b 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -208,11 +208,22 @@ describe Boards::IssuesController do end end - context 'with unauthorized user' do - it 'returns a forbidden 403 response' do - create_issue user: guest, board: board, list: list1, title: 'New issue' + context 'with guest user' do + context 'in open list' do + it 'returns a successful 200 response' do + open_list = board.lists.create(list_type: :backlog) + create_issue user: guest, board: board, list: open_list, title: 'New issue' - expect(response).to have_gitlab_http_status(403) + expect(response).to have_gitlab_http_status(200) + end + end + + context 'in label list' do + it 'returns a forbidden 403 response' do + create_issue user: guest, board: board, list: list1, title: 'New issue' + + expect(response).to have_gitlab_http_status(403) + end end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 56047c0c8d2..278b980b6d8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do end describe "#index" do + render_views + it 'returns group and project milestones to which the user belongs' do get :index, format: :json @@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id]) expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end + + it 'should contain group and project milestones to which the user belongs to' do + get :index + + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") + end end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index bf41aa0706f..f7a4a4192d6 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Groups::BoardsController do let(:group) { create(:group) } - let(:user) { create(:user) } + let(:user) { create(:user) } before do group.add_maintainer(user) @@ -22,6 +22,27 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to redirect_to(group_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards format: :html + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true) @@ -35,12 +56,30 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'return an array with one group board' do create(:board, group: group) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -74,7 +113,7 @@ describe Groups::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -93,10 +132,25 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 465f3499703..42723bb3820 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -63,7 +63,7 @@ describe Groups::MilestonesController do let(:group_milestone) { create(:milestone, group: group) } context 'when there is a title parameter' do - it 'searchs for a legacy group milestone' do + it 'searches for a legacy group milestone' do expect(GlobalMilestone).to receive(:build) expect(Milestone).not_to receive(:find_by_iid) @@ -72,7 +72,7 @@ describe Groups::MilestonesController do end context 'when there is not a title parameter' do - it 'searchs for a group milestone' do + it 'searches for a group milestone' do expect(GlobalMilestone).not_to receive(:build) expect(Milestone).to receive(:find_by_iid) diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a099cdafa58..4de61b65f71 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -529,7 +529,7 @@ describe GroupsController do sign_in(user) end - context 'when transfering to a subgroup goes right' do + context 'when transferring to a subgroup goes right' do let(:new_parent_group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 363ed410bc0..ea26bc83353 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -4,7 +4,7 @@ describe Profiles::KeysController do let(:user) { create(:user) } describe "#get_keys" do - describe "non existant user" do + describe "non existent user" do it "does not generally work" do get :get_keys, username: 'not-existent' diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 28f7e4634a5..64b589a6d83 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -331,10 +331,10 @@ describe Projects::BlobController do expect(response).to redirect_to( project_new_merge_request_path( forked_project, + merge_request_source_branch: "fork-test-1", merge_request: { source_project_id: forked_project.id, target_project_id: project.id, - source_branch: "fork-test-1", target_branch: "master" } ) diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 096efc1c7b2..667eaa5e34f 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -28,6 +28,27 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to redirect_to(namespace_project_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) @@ -41,12 +62,30 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns a list of project boards' do create_list(:board, 2, project: project) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -98,7 +137,7 @@ describe Projects::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -117,10 +156,25 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index b86029a4baf..bc17331f531 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do expect(response).to have_gitlab_http_status(200) end - it 'loads the terminals for the enviroment' do + it 'loads the terminals for the environment' do expect_any_instance_of(Environment).to receive(:terminals) get :terminal, environment_params diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 1484676eea3..8eb01145ed5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -152,11 +152,33 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z}) expect(json_response['new_issue_path']).to include('/issues/new') end end + context 'when job is running' do + context 'job is cancelable' do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + + it 'cancel_path is present with correct redirect' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path'])) + end + end + + context 'with web terminal' do + let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } + + it 'exposes the terminal path' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['terminal_path']).to match(%r{/terminal}) + end + end + end + context 'when job has artifacts' do context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -185,16 +207,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - context 'when job has terminal' do - let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - - it 'exposes the terminal path' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['terminal_path']).to match(%r{/terminal}) - end - end - context 'when job passed with no trace' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -297,6 +309,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be false + expect(json_response['stuck']).to be true end end @@ -309,6 +322,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be true + expect(json_response['stuck']).to be true end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index dcfd6c05200..7463586621c 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { fork_project(project, user, repository: true) } - let!(:environment) { create(:environment, project: forked) } - let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } - let(:admin) { create(:admin) } + let(:forked) { fork_project(project, user, repository: true) } + let(:sha) { forked.commit.sha } + let(:environment) { create(:environment, project: forked) } + let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) } let(:merge_request) do - create(:merge_request, source_project: forked, target_project: project) + create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline) end it 'links to the environment on that project' do @@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do expect(json_response.first['url']).to match /#{forked.full_path}/ end + context "when environment_target is 'merge_commit'" do + it 'returns nothing' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + + context 'when is merged' do + let(:source_environment) { create(:environment, project: project) } + let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") } + let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) } + let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) } + let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } + + before do + merge_request.update!(merge_commit_sha: merge_commit_sha) + merge_request.mark_as_merged! + end + + it 'returns the environment on the source project' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['url']).to match /#{project.full_path}/ + end + end + end + # we're trying to reduce the overall number of queries for this method. # set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287 it 'keeps queries in check' do @@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do expect(control_count).to be <= 137 end - def get_ci_environments_status - get :ci_environments_status, + def get_ci_environments_status(extra_params = {}) + params = { namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, - id: merge_request.iid, format: 'json' + id: merge_request.iid, + format: 'json' + } + + get :ci_environments_status, params.merge(extra_params) end end end diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb index 6114eef7003..00c1e617e3a 100644 --- a/spec/controllers/projects/mirrors_controller_spec.rb +++ b/spec/controllers/projects/mirrors_controller_spec.rb @@ -63,6 +63,69 @@ describe Projects::MirrorsController do end end + describe '#ssh_host_keys', :use_clean_rails_memory_store_caching do + let(:project) { create(:project) } + let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") } + + before do + sign_in(project.owner) + end + + context 'invalid URLs' do + %w[ + INVALID + git@example.com:foo/bar.git + ssh://git@example.com:foo/bar.git + ].each do |url| + it "returns an error with a 400 response for URL #{url.inspect}" do + do_get(project, url) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'Invalid URL') + end + end + end + + context 'no data in cache' do + it 'requests the cache to be filled and returns a 204 response' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once) + + do_get(project) + + expect(response).to have_gitlab_http_status(204) + end + end + + context 'error in the cache' do + it 'returns the error with a 400 response' do + stub_reactive_cache(cache, error: 'An error') + + do_get(project) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'An error') + end + end + + context 'data in the cache' do + let(:ssh_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf' } + let(:ssh_fp) { { type: 'ed25519', bits: 256, fingerprint: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', index: 0 } } + + it 'returns the data with a 200 response' do + stub_reactive_cache(cache, known_hosts: ssh_key) + + do_get(project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq('known_hosts' => ssh_key, 'fingerprints' => [ssh_fp.stringify_keys], 'host_keys_changed' => true) + end + end + + def do_get(project, url = 'ssh://example.com') + get :ssh_host_keys, namespace_id: project.namespace, project_id: project, ssh_url: url + end + end + def do_put(project, options, extra_attrs = {}) attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param) attrs[:project] = options diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index bcf289f36a9..6420b70a54f 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -5,6 +5,17 @@ shared_examples 'content not cached without revalidation' do end end +shared_examples 'content not cached without revalidation and no-store' do + it 'ensures content will not be cached without revalidation' do + # Fixed in newer versions of ActivePack, it will only output a single `private`. + if Gitlab.rails5? + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store') + else + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store') + end + end +end + describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -177,7 +188,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' @@ -239,7 +250,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -292,7 +303,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -344,7 +355,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -388,7 +399,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -445,7 +456,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' @@ -498,7 +509,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' diff --git a/spec/factories/board_group_recent_visit.rb b/spec/factories/board_group_recent_visit.rb new file mode 100644 index 00000000000..97ad5d6d068 --- /dev/null +++ b/spec/factories/board_group_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_group_recent_visit do + user + group + board + end +end diff --git a/spec/factories/board_project_recent_visit.rb b/spec/factories/board_project_recent_visit.rb new file mode 100644 index 00000000000..49ae4d7b391 --- /dev/null +++ b/spec/factories/board_project_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_project_recent_visit do + user + project + board + end +end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index bbeba8ce8b9..c9f5d0a813e 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -2,13 +2,28 @@ FactoryBot.define do factory :cluster, class: Clusters::Cluster do user name 'test-cluster' + cluster_type :project_type + + trait :instance do + cluster_type { Clusters::Cluster.cluster_types[:instance_type] } + end trait :project do + cluster_type { Clusters::Cluster.cluster_types[:project_type] } + before(:create) do |cluster, evaluator| cluster.projects << create(:project, :repository) end end + trait :group do + cluster_type { Clusters::Cluster.cluster_types[:group_type] } + + before(:create) do |cluster, evalutor| + cluster.groups << create(:group) + end + end + trait :provided_by_user do provider_type :user platform_type :kubernetes diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 134eb25e4b1..fe475e1f7a0 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -8,3 +8,4 @@ require_relative 'support/rspec' require 'active_support/all' ActiveSupport::Dependencies.autoload_paths << 'lib' +ActiveSupport::Dependencies.autoload_paths << 'ee/lib' diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 96dfde2e08c..735ca60f7da 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -53,13 +53,33 @@ describe 'Admin Groups' do expect_selected_visibility(internal) end - it 'when entered in group path, it auto filled the group name', :js do + it 'when entered in group name, it auto filled the group path', :js do visit admin_groups_path click_link "New group" - group_path = 'gitlab' + group_name = 'gitlab' + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq group_name + end + + it 'auto populates the group path with the group name', :js do + visit admin_groups_path + click_link "New group" + group_name = 'my gitlab project' + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq 'my-gitlab-project' + end + + it 'when entering in group path, group name does not change anymore', :js do + visit admin_groups_path + click_link "New group" + group_path = 'my-gitlab-project' + group_name = 'My modified gitlab project' fill_in 'group_path', with: group_path - name_field = find('input#group_name') - expect(name_field.value).to eq group_path + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq 'my-gitlab-project' end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 615223a2a88..2cdd3f55b50 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do it 'filters by unassigned' do set_filter('assignee') - click_filter_link('No Assignee') + click_filter_link('None') submit_filter page.within('.add-issues-modal') do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0bf1ecbc433..164442a47f5 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -94,8 +94,14 @@ describe 'Issue Boards new issue', :js do wait_for_requests end - it 'does not display new issue button' do - expect(page).to have_selector('.issue-count-badge-add-button', count: 0) + it 'displays new issue button in open list' do + expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + end + + it 'does not display new issue button in label list' do + page.within('.board:nth-child(2)') do + expect(page).not_to have_selector('.issue-count-badge-add-button') + end end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 8989b2051bb..5c6c1c4fd15 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -114,33 +114,6 @@ describe 'Commits' do expect(page).to have_content 'canceled' end end - - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do - visit pipeline_path(pipeline) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - - it 'shows warning' do - stub_ci_pipeline_yaml_file(nil) - visit pipeline_path(pipeline) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' - end - end - - context 'ci builds disabled' do - before do - stub_ci_builds_disabled - stub_ci_pipeline_yaml_file(nil) - visit pipeline_path(pipeline) - end - - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - end - end end context "when logged as reporter" do @@ -182,6 +155,39 @@ describe 'Commits' do end end end + + describe '.gitlab-ci.yml not found warning' do + before do + project.add_reporter(user) + end + + context 'ci builds enabled' do + it 'does not show warning' do + visit pipeline_path(pipeline) + + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_pipeline_yaml_file(nil) + + visit pipeline_path(pipeline) + + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end + end + + context 'ci builds disabled' do + it 'does not show warning' do + stub_ci_builds_disabled + stub_ci_pipeline_yaml_file(nil) + + visit pipeline_path(pipeline) + + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + end + end end context 'viewing commits for a branch' do diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index 6a0cd848345..d31df322d10 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Archived Project' do expect(page).not_to have_content(project.name) end - it 'searchs archived projects', :js do + it 'searches archived projects', :js do click_button 'Last updated' click_link 'Show archived projects' diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb index e57fcde8b2c..259f220c68b 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -14,15 +14,15 @@ RSpec.describe 'Dashboard Group' do it 'creates new group', :js do visit dashboard_groups_path find('.btn-success').click - new_path = 'Samurai' + new_name = 'Samurai' new_description = 'Tokugawa Shogunate' - fill_in 'group_path', with: new_path + fill_in 'group_name', with: new_name fill_in 'group_description', with: new_description click_button 'Create group' - expect(current_path).to eq group_path(Group.find_by(name: new_path)) - expect(page).to have_content(new_path) + expect(current_path).to eq group_path(Group.find_by(name: new_name)) + expect(page).to have_content(new_name) expect(page).to have_content(new_description) end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..0a24c5e906a 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -147,10 +147,12 @@ describe 'Dashboard Projects' do end context 'last push widget', :use_clean_rails_memory_store_caching do + let(:ref) { "feature" } + before do event = create(:push_event, project: project, author: user) - create(:push_event_payload, event: event, ref: 'feature', action: :created) + create(:push_event_payload, event: event, ref: ref, action: :created) Users::LastPushEventService.new(user).cache_last_push_event(event) @@ -165,9 +167,9 @@ describe 'Dashboard Projects' do end expect(page).to have_selector('.merge-request-form') - expect(current_path).to eq project_new_merge_request_path(project) + expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref) expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s - expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature' + expect(find('input#merge_request_source_branch', visible: false).value).to eq ref expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master' end end diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb index 11f05b6d220..259f22139ef 100644 --- a/spec/features/explore/new_menu_spec.rb +++ b/spec/features/explore/new_menu_spec.rb @@ -29,7 +29,7 @@ describe 'Top Plus Menu', :js do click_topmenuitem("New group") - expect(page).to have_content('Group path') + expect(page).to have_content('Group URL') expect(page).to have_content('Group name') end @@ -79,7 +79,7 @@ describe 'Top Plus Menu', :js do click_topmenuitem("New subgroup") - expect(page).to have_content('Group path') + expect(page).to have_content('Group URL') expect(page).to have_content('Group name') end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index e8ca6a6714f..174840794ed 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -95,9 +95,9 @@ describe 'Group milestones' do end it 'counts milestones correctly' do - expect(find('.top-area .active .badge').text).to eq("2") - expect(find('.top-area .closed .badge').text).to eq("2") - expect(find('.top-area .all .badge').text).to eq("4") + expect(find('.top-area .active .badge').text).to eq("3") + expect(find('.top-area .closed .badge').text).to eq("3") + expect(find('.top-area .all .badge').text).to eq("6") end it 'lists legacy group milestones and group milestones' do diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 63aa26cf5fd..4d04b8043ec 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -7,7 +7,7 @@ describe 'Group' do matcher :have_namespace_error_message do match do |page| - page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.") + page.has_content?("Group URL can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.") end end @@ -18,7 +18,7 @@ describe 'Group' do describe 'with space in group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'space group' + fill_in 'Group URL', with: 'space group' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -28,7 +28,7 @@ describe 'Group' do describe 'with .atom at end of group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'atom_group.atom' + fill_in 'Group URL', with: 'atom_group.atom' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -38,7 +38,7 @@ describe 'Group' do describe 'with .git at end of group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'git_group.git' + fill_in 'Group URL', with: 'git_group.git' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -94,7 +94,8 @@ describe 'Group' do end it 'creates a nested group' do - fill_in 'Group path', with: 'bar' + fill_in 'Group name', with: 'bar' + fill_in 'Group URL', with: 'bar' click_button 'Create group' expect(current_path).to eq(group_path('foo/bar')) @@ -112,7 +113,8 @@ describe 'Group' do visit new_group_path(group, parent_id: group.id) - fill_in 'Group path', with: 'bar' + fill_in 'Group name', with: 'bar' + fill_in 'Group URL', with: 'bar' click_button 'Create group' expect(current_path).to eq(group_path('foo/bar')) diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb index e381d073804..a90cdd8d920 100644 --- a/spec/features/import/manifest_import_spec.rb +++ b/spec/features/import/manifest_import_spec.rb @@ -22,7 +22,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js, :post expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint') end - it 'imports succesfully imports a project' do + it 'imports successfully imports a project' do visit new_import_manifest_path attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml')) diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index d011d2545bb..e910fb54d23 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do expect_filtered_search_input_empty end - it 'selects `no assignee`' do - find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + it 'selects `None`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click expect(page).to have_css(js_dropdown_assignee, visible: false) expect_tokens([assignee_token('none')]) expect_filtered_search_input_empty end + + it 'selects `Any`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect_tokens([assignee_token('any')]) + expect_filtered_search_input_empty + end end describe 'selecting from dropdown without Ajax call' do diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index be229e8aa7d..c42fcd92a36 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -92,7 +92,7 @@ describe 'Dropdown emoji', :js do it 'shows the most populated emoji at top of dropdown' do send_keys_to_filtered_search('my-reaction:') - expect(first('#js-dropdown-my-reaction li')).to have_content(award_emoji_star.name) + expect(first('#js-dropdown-my-reaction .filter-dropdown li')).to have_content(award_emoji_star.name) end end @@ -121,13 +121,29 @@ describe 'Dropdown emoji', :js do send_keys_to_filtered_search(':') end + it 'selects `None`' do + find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click + + expect(page).to have_css(js_dropdown_emoji, visible: false) + expect_tokens([reaction_token('none', false)]) + expect_filtered_search_input_empty + end + + it 'selects `Any`' do + find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click + + expect(page).to have_css(js_dropdown_emoji, visible: false) + expect_tokens([reaction_token('any', false)]) + expect_filtered_search_input_empty + end + it 'fills in the my-reaction name' do click_emoji('thumbsup') wait_for_requests expect(page).to have_css(js_dropdown_emoji, visible: false) - expect_tokens([emoji_token('thumbsup')]) + expect_tokens([reaction_token('thumbsup')]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 6ac7ccd00f7..1e1dd5691ab 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -118,7 +118,7 @@ describe 'Visual tokens', :js do describe 'selecting static option from dropdown' do before do - find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click + find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click end it 'changes value in visual token' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 08bf9bc7243..605860b90cd 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -15,7 +15,7 @@ describe 'GFM autocomplete', :js do wait_for_requests end - it 'updates issue descripton with GFM reference' do + it 'updates issue description with GFM reference' do find('.js-issuable-edit').click simulate_input('#issue-description', "@#{user.name[0...3]}") @@ -35,6 +35,21 @@ describe 'GFM autocomplete', :js do expect(page).to have_selector('.atwho-container') end + it 'opens autocomplete menu when field starts with text with item escaping HTML characters' do + alert_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' + create(:issue, project: project, title: alert_title) + + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('#') + end + + expect(page).to have_selector('.atwho-container') + + page.within '.atwho-container #at-view-issues' do + expect(page.all('li').first.text).to include(alert_title) + end + end + it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do find('#note-body').native.send_keys('testing') diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb index 40c452c991a..b0764db7751 100644 --- a/spec/features/issues/resource_label_events_spec.rb +++ b/spec/features/issues/resource_label_events_spec.rb @@ -7,6 +7,7 @@ describe 'List issue resource label events', :js do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, author: user) } let!(:label) { create(:label, project: project, title: 'foo') } + let!(:user_status) { create(:user_status, user: user) } context 'when user displays the issue' do let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') } @@ -23,6 +24,12 @@ describe 'List issue resource label events', :js do expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label' end end + + it 'shows the user status on the system note for the label' do + page.within("#note_#{event.discussion_id}") do + expect(page).to show_user_status user_status + end + end end context 'when user adds label to the issue' do diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb index 0ccab5b2fac..a124c99ecc8 100644 --- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb +++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb @@ -9,10 +9,10 @@ describe 'create a merge request, allowing commits from members who can merge to def visit_new_merge_request visit project_new_merge_request_path( source_project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: source_project.id, target_project_id: target_project.id, - source_branch: 'fix', target_branch: 'master' }) end diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index f744d7941f5..a298ead43db 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -3,15 +3,19 @@ require 'rails_helper' describe 'Merge request > User sees deployment widget', :js do describe 'when deployed to an environment' do let(:user) { create(:user) } - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, :merged, source_project: project) } let(:environment) { create(:environment, project: project) } let(:role) { :developer } - let(:sha) { project.commit('master').id } - let!(:deployment) { create(:deployment, environment: environment, sha: sha) } + let(:ref) { merge_request.target_branch } + let(:sha) { project.commit(ref).id } + let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do + merge_request.update!(merge_commit_sha: sha) project.add_user(user, role) sign_in(user) visit project_merge_request_path(project, merge_request) @@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do end context 'with stop action' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline) } let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - let(:deployment) do - create(:deployment, environment: environment, ref: merge_request.target_branch, - sha: sha, deployable: build, on_stop: 'close_app') - end before do + deployment.update!(on_stop: manual.name) wait_for_requests end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index f15129759de..0c610edd6d1 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -20,10 +20,10 @@ describe 'Merge request > User sees merge widget', :js do before do visit project_new_merge_request_path( project, + merge_request_source_branch: 'feature', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'feature', target_branch: 'master' }) end @@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do context 'view merge request' do let!(:environment) { create(:environment, project: project) } + let(:sha) { project.commit(merge_request.source_branch).sha } + let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } let!(:deployment) do create(:deployment, environment: environment, - ref: 'feature', - sha: merge_request.diff_head_sha) + ref: merge_request.source_branch, + deployable: build, + sha: sha) end before do + merge_request.update!(head_pipeline: pipeline) visit project_merge_request_path(project, merge_request) end it 'shows environments link' do wait_for_requests - page.within('.mr-widget-heading') do + page.within('.js-pre-merge-deploy') do expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) end @@ -174,7 +179,7 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again') + expect(page).to have_text(%r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) end end diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index 45cccbee63e..41f447fba95 100644 --- a/spec/features/merge_request/user_sees_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -41,8 +41,8 @@ describe 'Merge request > User sees pipelines', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page.find('.ci-widget')).to have_content( - 'Could not connect to the CI server. Please check your settings and try again') + expect(page.find('.ci-widget')).to have_text( + %r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) end end diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb index 92cc73ddf1f..6dfc819fe8a 100644 --- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb +++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb @@ -13,10 +13,10 @@ describe 'Merge request > User sees WIP help message' do it 'shows a specific WIP hint' do visit project_new_merge_request_path( project, + merge_request_source_branch: 'wip', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'wip', target_branch: 'master' }) @@ -32,10 +32,10 @@ describe 'Merge request > User sees WIP help message' do it 'shows the regular WIP message' do visit project_new_merge_request_path( project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'fix', target_branch: 'master' }) diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index ae41cf90576..147544740dc 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -109,13 +109,13 @@ describe 'Merge request > User selects branches for new MR', :js do end it 'populates source branch button' do - visit project_new_merge_request_path(project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' }) expect(find('.js-source-branch')).to have_content('fix') end it 'allows to change the diff view' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' }) click_link 'Changes' @@ -131,7 +131,7 @@ describe 'Merge request > User selects branches for new MR', :js do end it 'does not allow non-existing branches' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' }) expect(page).to have_content('The form contains the following errors') expect(page).to have_content('Source branch "non-exist-source" does not exist') @@ -140,7 +140,7 @@ describe 'Merge request > User selects branches for new MR', :js do context 'when a branch contains commits that both delete and add the same image' do it 'renders the diff successfully' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' }) click_link "Changes" @@ -165,7 +165,8 @@ describe 'Merge request > User selects branches for new MR', :js do it 'shows pipelines for a new merge request' do visit project_new_merge_request_path( project, - merge_request: { target_branch: 'master', source_branch: 'fix' }) + merge_request_source_branch: 'fix', + merge_request: { target_branch: 'master' }) page.within('.merge-request') do click_link 'Pipelines' diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb index b81478a481f..6e681185e1f 100644 --- a/spec/features/merge_request/user_uses_quick_actions_spec.rb +++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb @@ -144,7 +144,7 @@ describe 'Merge request > User uses quick actions', :js do describe '/target_branch command in merge request' do let(:another_project) { create(:project, :public, :repository) } - let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + let(:new_url_opts) { { merge_request_source_branch: 'feature' } } before do another_project.add_maintainer(user) diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb index ec1153b7f7f..8ecdec491b8 100644 --- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb +++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb @@ -65,7 +65,7 @@ describe 'User squashes a merge request', :js do context 'when squash is enabled on merge request creation' do before do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' }) check 'merge_request[squash]' click_on 'Submit merge request' wait_for_requests @@ -95,7 +95,7 @@ describe 'User squashes a merge request', :js do context 'when squash is not enabled on merge request creation' do before do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' }) click_on 'Submit merge request' wait_for_requests end diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb index 8c4488b2ca6..dee81898928 100644 --- a/spec/features/projects/badges/pipeline_badge_spec.rb +++ b/spec/features/projects/badges/pipeline_badge_spec.rb @@ -19,7 +19,7 @@ describe 'Pipeline Badge' do let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: ref, sha: project.commit(ref).sha) } let!(:job) { create(:ci_build, pipeline: pipeline) } - context 'when the pipeline was successfull' do + context 'when the pipeline was successful' do it 'displays so on the badge' do job.success diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 91eac9c8278..f13c35c00d3 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -40,7 +40,7 @@ describe 'Clusters', :js do expect(page).to have_selector('.js-project-feature-toggle') end - context 'with sucessfull request' do + context 'with successful request' do it 'user sees updated cluster' do expect do page.find('.js-project-feature-toggle').click diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 847b5f0860f..6f620dff82b 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -57,7 +57,7 @@ describe 'Projects > Files > User creates a directory', :js do expect(page).to have_content('From new-feature into master') expect(page).to have_content('Add new directory') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature")) end end @@ -80,8 +80,7 @@ describe 'Projects > Files > User creates a directory', :js do click_button('Create directory') fork = user.fork_of(project2.reload) - - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) end end end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index d4dda43c823..14b5bd58bd1 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -16,7 +16,7 @@ describe 'Projects > Files > User creates files' do sign_in(user) end - context 'without commiting a new file' do + context 'without committing a new file' do context 'when an user has write access' do before do visit(project_tree_path_root_ref) @@ -49,7 +49,7 @@ describe 'Projects > Files > User creates files' do end end - context 'with commiting a new file' do + context 'with committing a new file' do context 'when an user has write access' do before do visit(project_tree_path_root_ref) @@ -144,7 +144,7 @@ describe 'Projects > Files > User creates files' do fill_in(:branch_name, with: 'new_branch_name', visible: true) click_button('Commit changes') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') @@ -182,7 +182,7 @@ describe 'Projects > Files > User creates files' do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) expect(page).to have_content('New commit message') end end diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 614b11fa5c8..faf11ee9dd8 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -63,7 +63,7 @@ describe 'Projects > Files > User deletes files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) expect(page).to have_content('New commit message') end end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 9eb65ec159c..c6b2aaea906 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -86,7 +86,7 @@ describe 'Projects > Files > User edits files', :js do fill_in(:branch_name, with: 'new_branch_name', visible: true) click_button('Commit changes') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') @@ -155,7 +155,7 @@ describe 'Projects > Files > User edits files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) wait_for_requests @@ -183,7 +183,7 @@ describe 'Projects > Files > User edits files', :js do fork = user.fork_of(project2) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) wait_for_requests diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index e3da28d73c3..09feb315465 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -78,7 +78,7 @@ describe 'Projects > Files > User replaces files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined")) click_link('Changes') diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb index af3fc528a20..92df8303f46 100644 --- a/spec/features/projects/files/user_uploads_files_spec.rb +++ b/spec/features/projects/files/user_uploads_files_spec.rb @@ -36,7 +36,7 @@ describe 'Projects > Files > User uploads files' do click_button('Upload file') expect(page).to have_content('New commit message') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') find("a[data-action='diffs']", text: 'Changes').click @@ -92,7 +92,7 @@ describe 'Projects > Files > User uploads files' do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined")) find("a[data-action='diffs']", text: 'Changes').click diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index c3902ecdd17..5cb3f7c732f 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -198,6 +198,24 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end + context 'when job is running', :js do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + let(:job_url) { project_job_path(project, job) } + + before do + visit job_url + wait_for_requests + end + + context 'job is cancelable' do + it 'shows cancel button' do + click_link 'Cancel' + + expect(page.current_path).to eq(job_url) + end + end + end + context "Job from other project" do before do visit project_job_path(project, job2) @@ -721,6 +739,62 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased') end end + + context 'stuck', :js do + before do + visit project_job_path(project, job) + wait_for_requests + end + + context 'without active runners available' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because no runners are active' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'when available runners can not run specified tag' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'when runners are offline and build has tags' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'without any runners available' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline) } + + it 'renders message about job being stuck because not runners are available' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'without available runners online' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because runners are offline' do + expect(page).to have_css('.js-stuck-no-runners') + expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.") + end + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 69561b4d733..4be31511ceb 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -22,8 +22,8 @@ describe 'Merge Request button' do it 'shows Create merge request button' do href = project_new_merge_request_path(project, - merge_request: { source_branch: 'feature', - target_branch: 'master' }) + merge_request_source_branch: 'feature', + merge_request: { target_branch: 'master' }) visit url @@ -77,8 +77,8 @@ describe 'Merge Request button' do it 'shows Create merge request button' do href = project_new_merge_request_path(forked_project, - merge_request: { source_branch: 'feature', - target_branch: 'master' }) + merge_request_source_branch: 'feature', + merge_request: { target_branch: 'master' }) visit fork_url diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index fb766addb31..0add129dde2 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -322,6 +322,22 @@ describe 'Project' do end end + context 'content is not cached after signing out', :js do + let(:user) { create(:user, project_view: 'activity') } + let(:project) { create(:project, :repository) } + + it 'does not load activity', :js do + project.add_maintainer(user) + sign_in(user) + visit project_path(project) + sign_out(user) + + page.evaluate_script('window.history.back()') + + expect(page).not_to have_selector('.event-item') + end + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 3ee753b7d23..7225ca65492 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'User searches for wiki pages', :js do let(:user) { create(:user) } - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) } before do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 0689c843104..c0488c83bd8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -57,13 +57,39 @@ describe IssuesFinder do end context 'filtering by no assignee' do - let(:params) { { assignee_id: 0 } } + let(:params) { { assignee_id: 'None' } } + + it 'returns issues not assigned to any assignee' do + expect(issues).to contain_exactly(issue4) + end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 0 + + expect(issues).to contain_exactly(issue4) + end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 'none' - it 'returns issues not assign to any assignee' do expect(issues).to contain_exactly(issue4) end end + context 'filtering by any assignee' do + let(:params) { { assignee_id: 'Any' } } + + it 'returns issues assigned to any assignee' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + + it 'returns issues assigned to any assignee' do + params[:assignee_id] = 'any' + + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + end + context 'filtering by group_id' do let(:params) { { group_id: group.id } } @@ -118,19 +144,31 @@ describe IssuesFinder do end context 'filtering by no milestone' do - let(:params) { { milestone_title: Milestone::None.title } } + let(:params) { { milestone_title: 'None' } } it 'returns issues with no milestone' do expect(issues).to contain_exactly(issue2, issue3, issue4) end + + it 'returns issues with no milestone (deprecated)' do + params[:milestone_title] = Milestone::None.title + + expect(issues).to contain_exactly(issue2, issue3, issue4) + end end context 'filtering by any milestone' do - let(:params) { { milestone_title: Milestone::Any.title } } + let(:params) { { milestone_title: 'Any' } } it 'returns issues with any assigned milestone' do expect(issues).to contain_exactly(issue1) end + + it 'returns issues with any assigned milestone (deprecated)' do + params[:milestone_title] = Milestone::Any.title + + expect(issues).to contain_exactly(issue1) + end end context 'filtering by upcoming milestone' do @@ -334,6 +372,22 @@ describe IssuesFinder do end context 'filtering by reaction name' do + context 'user searches by no reaction' do + let(:params) { { my_reaction_emoji: 'None' } } + + it 'returns issues that the user did not react to' do + expect(issues).to contain_exactly(issue2, issue4) + end + end + + context 'user searches by any reaction' do + let(:params) { { my_reaction_emoji: 'Any' } } + + it 'returns issues that the user reacted to' do + expect(issues).to contain_exactly(issue1, issue3) + end + end + context 'user searches by "thumbsup" reaction' do let(:params) { { my_reaction_emoji: 'thumbsup' } } diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index c40977bc4ee..35971d564d5 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -46,6 +46,7 @@ "diff_head_commit_short_id": { "type": ["string", "null"] }, "merge_commit_message": { "type": ["string", "null"] }, "pipeline": { "type": ["object", "null"] }, + "merge_pipeline": { "type": ["object", "null"] }, "work_in_progress": { "type": "boolean" }, "source_branch_exists": { "type": "boolean" }, "mergeable_discussions_state": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 8218474705c..cdf7b049ab6 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -18,6 +18,7 @@ "runner": { "$ref": "runner.json" }, "runners": { "$ref": "runners.json" }, "has_trace": { "type": "boolean" }, - "stage": { "type": "string" } + "stage": { "type": "string" }, + "stuck": { "type": "boolean" } } } diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 1c216b3fe97..f709f152c92 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -3,63 +3,13 @@ require 'spec_helper' describe BlobHelper do include TreeHelper - let(:blob_name) { 'test.lisp' } - let(:no_context_content) { ":type \"assem\"))" } - let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } - let(:split_content) { blob_content.split("\n") } - let(:multiline_content) do - %q( - def test(input): - """This is line 1 of a multi-line comment. - This is line 2. - """ - ) - end - describe '#highlight' do - it 'returns plaintext for unknown lexer context' do - result = helper.highlight(blob_name, no_context_content) - expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>]) + it 'wraps highlighted content' do + expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>]) end - it 'returns plaintext for long blobs' do - stub_const('Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1) - result = helper.highlight(blob_name, blob_content) - - expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span></code></pre>]) - end - - it 'highlights single block' do - expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> -<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] - - expect(helper.highlight(blob_name, blob_content)).to eq(expected) - end - - it 'highlights multi-line comments' do - result = helper.highlight(blob_name, multiline_content) - html = Nokogiri::HTML(result) - lines = html.search('.s') - expect(lines.count).to eq(3) - expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') - expect(lines[1].text).to eq(' This is line 2.') - expect(lines[2].text).to eq(' """') - end - - context 'diff highlighting' do - let(:blob_name) { 'test.diff' } - let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} - let(:expected) do - %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> -<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> -<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span> -<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>) - end - - it 'highlights each line properly' do - result = helper.highlight(blob_name, blob_content) - expect(result).to eq(expected) - end + it 'handles plain version' do + expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>]) end end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index b0689fc7cfe..ce5d2022441 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -219,7 +219,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -244,7 +244,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 037e06cf3b2..2642c8b1bdb 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -18,7 +18,7 @@ describe('Board list component', () => { let mock; let component; - beforeEach((done) => { + beforeEach(done => { const el = document.createElement('div'); document.body.appendChild(el); @@ -62,122 +62,102 @@ describe('Board list component', () => { }); it('renders component', () => { - expect( - component.$el.classList.contains('board-list-component'), - ).toBe(true); + expect(component.$el.classList.contains('board-list-component')).toBe(true); }); - it('renders loading icon', (done) => { + it('renders loading icon', done => { component.loading = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-loading'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-loading')).not.toBeNull(); done(); }); }); it('renders issues', () => { - expect( - component.$el.querySelectorAll('.board-card').length, - ).toBe(1); + expect(component.$el.querySelectorAll('.board-card').length).toBe(1); }); it('sets data attribute with issue id', () => { - expect( - component.$el.querySelector('.board-card').getAttribute('data-issue-id'), - ).toBe('1'); + expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1'); }); - it('shows new issue form', (done) => { + it('shows new issue form', done => { component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('shows new issue form after eventhub event', (done) => { + it('shows new issue form after eventhub event', done => { eventHub.$emit(`hide-issue-form-${component.list.id}`); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('does not show new issue form for closed list', (done) => { + it('does not show new issue form for closed list', done => { component.list.type = 'closed'; component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).toBeNull(); done(); }); }); - it('shows count list item', (done) => { + it('shows count list item', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count')).not.toBeNull(); - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing all issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing all issues', + ); done(); }); }); - it('sets data attribute with invalid id', (done) => { + it('sets data attribute with invalid id', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), - ).toBe('-1'); + expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe( + '-1', + ); done(); }); }); - it('shows how many more issues to load', (done) => { + it('shows how many more issues to load', done => { component.showCount = true; component.list.issuesSize = 20; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing 1 of 20 issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing 1 of 20 issues', + ); done(); }); }); - it('loads more issues after scrolling', (done) => { + it('loads more issues after scrolling', done => { spyOn(component.list, 'nextPage'); component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; @@ -200,7 +180,9 @@ describe('Board list component', () => { }); it('does not load issues if already loading', () => { - component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); + component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue( + new Promise(() => {}), + ); component.onScroll(); component.onScroll(); @@ -208,14 +190,12 @@ describe('Board list component', () => { expect(component.list.nextPage).toHaveBeenCalledTimes(1); }); - it('shows loading more spinner', (done) => { + it('shows loading more spinner', done => { component.showCount = true; component.list.loadingMore = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count .fa-spinner'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index d4c53bd5a7d..dee7841c088 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -8,7 +8,7 @@ describe('Board component', () => { let vm; let el; - beforeEach((done) => { + beforeEach(done => { loadFixtures('boards/show.html.raw'); el = document.createElement('div'); @@ -50,56 +50,46 @@ describe('Board component', () => { }); it('board is expandable when list type is backlog', () => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); }); - it('board is expandable when list type is closed', (done) => { + it('board is expandable when list type is closed', done => { vm.list.type = 'closed'; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); done(); }); }); - it('board is not expandable when list type is label', (done) => { + it('board is not expandable when list type is label', done => { vm.list.type = 'label'; vm.list.isExpandable = false; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(false); + expect(vm.$el.classList.contains('is-expandable')).toBe(false); done(); }); }); - it('collapses when clicking header', (done) => { + it('collapses when clicking header', done => { vm.$el.querySelector('.board-header').click(); Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); }); }); - it('created sets isExpanded to true from localStorage', (done) => { + it('created sets isExpanded to true from localStorage', done => { vm.$el.querySelector('.board-header').click(); return Vue.nextTick() .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); // call created manually vm.$options.created[0].call(vm); @@ -107,11 +97,10 @@ describe('Board component', () => { return Vue.nextTick(); }) .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index 4f8701bae01..1fc0e206d5e 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -24,7 +24,7 @@ describe('AjaxFormVariableList', () => { mock = new MockAdapter(axios); const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); - saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); errorBox = container.querySelector('.js-ci-variable-error-box'); ajaxVariableList = new AjaxFormVariableList({ container, @@ -44,7 +44,7 @@ describe('AjaxFormVariableList', () => { describe('onSaveClicked', () => { it('shows loading spinner while waiting for the request', done => { - const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); + const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon'); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false); @@ -172,7 +172,7 @@ describe('AjaxFormVariableList', () => { container = document.querySelector('.js-ci-variable-list-section'); const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); - saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); errorBox = container.querySelector('.js-ci-variable-error-box'); ajaxVariableList = new AjaxFormVariableList({ container, diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 4fc56fd9a27..f6b36e88a5f 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -22,7 +22,7 @@ describe('Commit pipeline status component', () => { Component = Vue.extend(commitPipelineStatus); }); - describe('While polling pipeline data succesfully', () => { + describe('While polling pipeline data successfully', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(() => { @@ -59,14 +59,14 @@ describe('Commit pipeline status component', () => { }); }); - it('contains a ciStatus when the polling is succesful ', done => { + it('contains a ciStatus when the polling is successful ', done => { setTimeout(() => { expect(vm.ciStatus).toEqual(mockCiStatus); done(); }); }); - it('contains a ci-status icon when polling is succesful', done => { + it('contains a ci-status icon when polling is successful', done => { setTimeout(() => { expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); expect(vm.$el.querySelector('.ci-status-icon').classList).toContain( @@ -77,7 +77,7 @@ describe('Commit pipeline status component', () => { }); }); - describe('When polling data was not succesful', () => { + describe('When polling data was not successful', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(502, {}); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 7237274eb43..d9d7f61785f 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -1 +1,125 @@ -// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import Vue from 'vue'; +import CompareVersionsComponent from '~/diffs/components/compare_versions.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +describe('CompareVersions', () => { + let vm; + const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, { + mergeRequestDiffs: diffsMockData, + mergeRequestDiff: diffsMockData[0], + targetBranch, + }).$mount(); + }); + + describe('template', () => { + it('should render Tree List toggle button with correct attribute values', () => { + const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list'); + + expect(treeListBtn).not.toBeNull(); + expect(treeListBtn.dataset.originalTitle).toBe('Toggle file browser'); + expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain( + '#hamburger', + ); + }); + + it('should render comparison dropdowns with correct values', () => { + const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown'); + const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown'); + + expect(sourceDropdown).not.toBeNull(); + expect(targetDropdown).not.toBeNull(); + expect(sourceDropdown.querySelector('a span').innerHTML).toContain('latest version'); + expect(targetDropdown.querySelector('a span').innerHTML).toContain(targetBranch.branchName); + }); + + it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => { + vm.mergeRequestDiffs = []; + + vm.$nextTick(() => { + const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown'); + const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown'); + + expect(sourceDropdown).toBeNull(); + expect(targetDropdown).toBeNull(); + }); + }); + + it('should render whitespace toggle button with correct attributes', () => { + const whitespaceBtn = vm.$el.querySelector('.qa-toggle-whitespace'); + const href = vm.toggleWhitespacePath; + + expect(whitespaceBtn).not.toBeNull(); + expect(whitespaceBtn.getAttribute('href')).toEqual(href); + expect(whitespaceBtn.innerHTML).toContain('Hide whitespace changes'); + }); + + it('should render view types buttons with correct values', () => { + const inlineBtn = vm.$el.querySelector('#inline-diff-btn'); + const parallelBtn = vm.$el.querySelector('#parallel-diff-btn'); + + expect(inlineBtn).not.toBeNull(); + expect(parallelBtn).not.toBeNull(); + expect(inlineBtn.dataset.viewType).toEqual('inline'); + expect(parallelBtn.dataset.viewType).toEqual('parallel'); + expect(inlineBtn.innerHTML).toContain('Inline'); + expect(parallelBtn.innerHTML).toContain('Side-by-side'); + }); + }); + + describe('setInlineDiffViewType', () => { + it('should persist the view type in the url', () => { + const viewTypeBtn = vm.$el.querySelector('#inline-diff-btn'); + viewTypeBtn.click(); + + expect(window.location.toString()).toContain('?view=inline'); + }); + }); + + describe('setParallelDiffViewType', () => { + it('should persist the view type in the url', () => { + const viewTypeBtn = vm.$el.querySelector('#parallel-diff-btn'); + viewTypeBtn.click(); + + expect(window.location.toString()).toContain('?view=parallel'); + }); + }); + + describe('comparableDiffs', () => { + it('should not contain the first item in the mergeRequestDiffs property', () => { + const { comparableDiffs } = vm; + const comparableDiffsMock = diffsMockData.slice(1); + + expect(comparableDiffs).toEqual(comparableDiffsMock); + }); + }); + + describe('isWhitespaceVisible', () => { + const originalHref = window.location.href; + + afterEach(() => { + window.history.replaceState({}, null, originalHref); + }); + + it('should return "true" when no "w" flag is present in the URL (default)', () => { + expect(vm.isWhitespaceVisible()).toBe(true); + }); + + it('should return "false" when the flag is set to "1" in the URL', () => { + window.history.replaceState({}, null, '?w=1'); + + expect(vm.isWhitespaceVisible()).toBe(false); + }); + + it('should return "true" when the flag is set to "0" in the URL', () => { + window.history.replaceState({}, null, '?w=0'); + + expect(vm.isWhitespaceVisible()).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 08e25d2004e..fc94d0bab5b 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -53,7 +53,7 @@ describe('Diffs tree list component', () => { fileHash: 'test', key: 'index.js', name: 'index.js', - path: 'index.js', + path: 'app/index.js', removedLines: 0, tempFile: true, type: 'blob', @@ -104,7 +104,55 @@ describe('Diffs tree list component', () => { vm.$el.querySelector('.file-row').click(); - expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js'); + expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/index.js'); + }); + + it('renders as file list when renderTreeList is false', done => { + vm.renderTreeList = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.file-row').length).toBe(1); + + done(); + }); + }); + + it('renders file paths when renderTreeList is false', done => { + vm.renderTreeList = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-row').textContent).toContain('app/index.js'); + + done(); + }); + }); + + it('hides render buttons when input is focused', done => { + const focusEvent = new Event('focus'); + + vm.$el.querySelector('.form-control').dispatchEvent(focusEvent); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none'); + + done(); + }); + }); + + it('shows render buttons when input is blurred', done => { + const blurEvent = new Event('blur'); + vm.focusSearch = true; + + vm.$nextTick() + .then(() => { + vm.$el.querySelector('.form-control').dispatchEvent(blurEvent); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none'); + }) + .then(done) + .catch(done.fail); }); }); @@ -117,4 +165,24 @@ describe('Diffs tree list component', () => { expect(vm.search).toBe(''); }); }); + + describe('toggleRenderTreeList', () => { + it('updates renderTreeList', () => { + expect(vm.renderTreeList).toBe(true); + + vm.toggleRenderTreeList(false); + + expect(vm.renderTreeList).toBe(false); + }); + }); + + describe('toggleFocusSearch', () => { + it('updates focusSearch', () => { + expect(vm.focusSearch).toBe(false); + + vm.toggleFocusSearch(true); + + expect(vm.focusSearch).toBe(true); + }); + }); }); diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js new file mode 100644 index 00000000000..d72ad7818dd --- /dev/null +++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js @@ -0,0 +1,42 @@ +export default [ + { + versionIndex: 4, + createdAt: '2018-10-23T11:49:16.611Z', + commitsCount: 4, + latest: true, + shortCommitSha: 'de7a8f7f', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995', + }, + { + versionIndex: 3, + createdAt: '2018-10-23T11:46:40.617Z', + commitsCount: 3, + latest: false, + shortCommitSha: 'e78fc18f', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e', + }, + { + versionIndex: 2, + createdAt: '2018-10-04T09:57:39.648Z', + commitsCount: 2, + latest: false, + shortCommitSha: '48da7e7e', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e', + }, + { + versionIndex: 1, + createdAt: '2018-09-25T20:30:39.493Z', + commitsCount: 1, + latest: false, + shortCommitSha: '47bac2ed', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0', + }, +]; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 85c1926fcb1..bb623953710 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -27,7 +27,6 @@ import actions, { toggleShowTreeList, } from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; -import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import axios from '~/lib/utils/axios_utils'; import testAction from '../../helpers/vuex_action_helper'; @@ -152,7 +151,7 @@ describe('DiffsStoreActions', () => { original_position: diffPosition, }; - const discussions = reduceDiscussionsToLineCodes([singleDiscussion]); + const discussions = [singleDiscussion]; testAction( assignDiscussionsToDiff, @@ -162,8 +161,7 @@ describe('DiffsStoreActions', () => { { type: types.SET_LINE_DISCUSSIONS_FOR_FILE, payload: { - fileHash: 'ABC', - discussions: [singleDiscussion], + discussion: singleDiscussion, diffPositionByLineCode: { ABC_1_1: { baseSha: 'abc', @@ -581,7 +579,6 @@ describe('DiffsStoreActions', () => { describe('saveDiffDiscussion', () => { beforeEach(() => { spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData'); - spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions'); }); it('dispatches actions', done => { @@ -602,7 +599,7 @@ describe('DiffsStoreActions', () => { .then(() => { expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]); expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); - expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']); + expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index b7e28391419..fed04cbaed8 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -198,40 +198,33 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].right.discussions).toEqual([]); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); it('should add legacy discussions to the given line', () => { @@ -272,36 +265,30 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - active: true, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - active: true, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index ef367fc09fa..f49dee3696d 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -445,6 +445,14 @@ describe('DiffsStoreUtils', () => { fileHash: 'test', }, { + newPath: 'app/test/filepathneedstruncating.js', + deletedFile: false, + newFile: true, + removedLines: 0, + addedLines: 0, + fileHash: 'test', + }, + { newPath: 'package.json', deletedFile: true, newFile: false, @@ -498,6 +506,19 @@ describe('DiffsStoreUtils', () => { type: 'blob', tree: [], }, + { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'test', + key: 'app/test/filepathneedstruncating.js', + name: 'filepathneedstruncating.js', + path: 'app/test/filepathneedstruncating.js', + removedLines: 0, + tempFile: true, + type: 'blob', + tree: [], + }, ], }, ], @@ -527,6 +548,7 @@ describe('DiffsStoreUtils', () => { 'app/index.js', 'app/test', 'app/test/index.js', + 'app/test/filepathneedstruncating.js', 'package.json', ]); }); diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js index d71dfe8197e..1f986d49bc7 100644 --- a/spec/javascripts/environments/emtpy_state_spec.js +++ b/spec/javascripts/environments/emtpy_state_spec.js @@ -1,4 +1,3 @@ - import Vue from 'vue'; import emptyState from '~/environments/components/empty_state.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -25,13 +24,13 @@ describe('environments empty state', () => { }); it('renders empty state and new environment button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), - ).toEqual('foo'); + expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual( + 'foo', + ); }); }); @@ -45,13 +44,11 @@ describe('environments empty state', () => { }); it('renders empty state without new button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull(); }); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0b933dda431..7618c2f50ce 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -38,7 +38,9 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain( + mockItem.size, + ); }); }); @@ -68,7 +70,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit: { @@ -84,7 +87,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', @@ -121,18 +125,18 @@ describe('Environment item', () => { }); it('should render environment name', () => { - expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name); + expect(component.$el.querySelector('.environment-name').textContent).toContain( + environment.name, + ); }); describe('With deployment', () => { it('should render deployment internal id', () => { - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain(environment.last_deployment.iid); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain( + environment.last_deployment.iid, + ); - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain('#'); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#'); }); it('should render last deployment date', () => { @@ -156,56 +160,46 @@ describe('Environment item', () => { describe('With build url', () => { it('Should link to build url provided', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); it('Should render deployable name and id', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); }); describe('With commit information', () => { it('should render commit component', () => { - expect( - component.$el.querySelector('.js-commit-component'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-commit-component')).toBeDefined(); }); }); }); describe('With manual actions', () => { it('Should render actions component', () => { - expect( - component.$el.querySelector('.js-manual-actions-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined(); }); }); describe('With external URL', () => { it('should render external url component', () => { - expect( - component.$el.querySelector('.js-external-url-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-external-url-container')).toBeDefined(); }); }); describe('With stop action', () => { it('Should render stop action component', () => { - expect( - component.$el.querySelector('.js-stop-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined(); }); }); describe('With retry action', () => { it('Should render rollback component', () => { - expect( - component.$el.querySelector('.js-rollback-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined(); }); }); }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index 7edc0ccac0b..e2d81eb454a 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -31,9 +31,9 @@ describe('Environment', () => { mock.restore(); }); - describe('successfull request', () => { + describe('successful request', () => { describe('without environments', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(200, { environments: [] }); component = mountComponent(EnvironmentsComponent, mockData); @@ -44,30 +44,34 @@ describe('Environment', () => { }); it('should render the empty state', () => { - expect( - component.$el.querySelector('.js-new-environment-button').textContent, - ).toContain('New environment'); + expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain( + 'New environment', + ); - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('with paginated environments', () => { - beforeEach((done) => { - mock.onGet(mockData.endpoint).reply(200, { - environments: [environment], - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '1', - 'X-Prev-Page': '', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', - }); + beforeEach(done => { + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: [environment], + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + ); component = mountComponent(EnvironmentsComponent, mockData); @@ -78,19 +82,17 @@ describe('Environment', () => { it('should render a table with environments', () => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environment.name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environment.name, + ); }); describe('pagination', () => { it('should render pagination', () => { - expect( - component.$el.querySelectorAll('.gl-pagination li').length, - ).toEqual(5); + expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5); }); - it('should make an API request when page is clicked', (done) => { + it('should make an API request when page is clicked', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); @@ -100,7 +102,7 @@ describe('Environment', () => { }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -114,7 +116,7 @@ describe('Environment', () => { }); describe('unsuccessfull request', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(500, {}); component = mountComponent(EnvironmentsComponent, mockData); @@ -125,15 +127,16 @@ describe('Environment', () => { }); it('should render empty state', () => { - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('expandable folders', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [folder], stopped_count: 0, @@ -154,7 +157,7 @@ describe('Environment', () => { component = mountComponent(EnvironmentsComponent, mockData); }); - it('should open a closed folder', (done) => { + it('should open a closed folder', done => { setTimeout(() => { component.$el.querySelector('.folder-name').click(); @@ -165,7 +168,7 @@ describe('Environment', () => { }, 0); }); - it('should close an opened folder', (done) => { + it('should close an opened folder', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -182,7 +185,7 @@ describe('Environment', () => { }, 0); }); - it('should show children environments and a button to show all environments', (done) => { + it('should show children environments and a button to show all environments', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -191,7 +194,9 @@ describe('Environment', () => { // wait for next async request setTimeout(() => { expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); - expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); + expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain( + 'Show all', + ); done(); }); }); @@ -201,7 +206,8 @@ describe('Environment', () => { describe('methods', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [], stopped_count: 0, @@ -215,8 +221,9 @@ describe('Environment', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '3' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '3' }) .then(() => { expect(component.page).toEqual('3'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 51d4213c38f..7f0a9475d5f 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -30,39 +30,43 @@ describe('Environments Folder View', () => { component.$destroy(); }); - describe('successfull request', () => { + describe('successful request', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, { - environments: environmentsList, - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '2', - 'X-Prev-Page': '', - 'X-TOTAL': '20', - 'X-Total-Pages': '10', - }); + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '20', + 'X-Total-Pages': '10', + }, + ); component = mountComponent(Component, mockData); }); - it('should render a table with environments', (done) => { + it('should render a table with environments', done => { setTimeout(() => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environmentsList[0].name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environmentsList[0].name, + ); done(); }, 0); }); - it('should render available tab with count', (done) => { + it('should render available tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -71,11 +75,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count', (done) => { + it('should render stopped tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -84,36 +88,37 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render parent folder name', (done) => { + it('should render parent folder name', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-folder-name').textContent.trim(), - ).toContain('Environments / review'); + expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain( + 'Environments / review', + ); done(); }, 0); }); describe('pagination', () => { - it('should render pagination', (done) => { + it('should render pagination', done => { setTimeout(() => { - expect( - component.$el.querySelectorAll('.gl-pagination'), - ).not.toBeNull(); + expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull(); done(); }, 0); }); - it('should make an API request when changing page', (done) => { + it('should make an API request when changing page', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination .js-last-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' }); + expect(component.updateContent).toHaveBeenCalledWith({ + scope: component.scope, + page: '10', + }); done(); }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -134,20 +139,18 @@ describe('Environments Folder View', () => { component = mountComponent(Component, mockData); }); - it('should not render a table', (done) => { + it('should not render a table', done => { setTimeout(() => { - expect( - component.$el.querySelector('table'), - ).toBe(null); + expect(component.$el.querySelector('table')).toBe(null); done(); }, 0); }); - it('should render available tab with count 0', (done) => { + it('should render available tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -156,11 +159,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count 0', (done) => { + it('should render stopped tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -181,8 +184,9 @@ describe('Environments Folder View', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '4' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '4' }) .then(() => { expect(component.page).toEqual('4'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index d7338ee0f66..aecab331ead 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -172,7 +172,7 @@ describe('Flash', () => { flash('test'); expect(document.querySelector('.flash-text').className).toBe( - 'flash-text container-fluid container-limited', + 'flash-text container-fluid container-limited limit-container-width', ); }); @@ -180,7 +180,7 @@ describe('Flash', () => { document.querySelector('.content-wrapper').className = 'js-content-wrapper'; flash('test'); - expect(document.querySelector('.flash-text').className.trim()).toBe('flash-text'); + expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text'); }); it('removes element after clicking', () => { diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index 9e6f236b687..9754c8a6755 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -25,25 +25,21 @@ describe('Title component', () => { }); it('renders title HTML', () => { - expect( - vm.$el.querySelector('.title').innerHTML.trim(), - ).toBe('Testing <img>'); + expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); }); - it('updates page title when changing titleHtml', (done) => { + it('updates page title when changing titleHtml', done => { spyOn(vm, 'setPageTitle'); vm.titleHtml = 'test'; Vue.nextTick(() => { - expect( - vm.setPageTitle, - ).toHaveBeenCalled(); + expect(vm.setPageTitle).toHaveBeenCalled(); done(); }); }); - it('animates title changes', (done) => { + it('animates title changes', done => { vm.titleHtml = 'test'; Vue.nextTick(() => { @@ -61,14 +57,12 @@ describe('Title component', () => { }); }); - it('updates page title after changing title', (done) => { + it('updates page title after changing title', done => { vm.titleHtml = 'changed'; vm.titleText = 'changed'; Vue.nextTick(() => { - expect( - document.querySelector('title').textContent.trim(), - ).toContain('changed'); + expect(document.querySelector('title').textContent.trim()).toContain('changed'); done(); }); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js deleted file mode 100644 index bc973407b25..00000000000 --- a/spec/javascripts/job_spec.js +++ /dev/null @@ -1,265 +0,0 @@ -// import $ from 'jquery'; -// import MockAdapter from 'axios-mock-adapter'; -// import axios from '~/lib/utils/axios_utils'; -// import { numberToHumanSize } from '~/lib/utils/number_utils'; -// import '~/lib/utils/datetime_utility'; -// import Job from '~/job'; -// import '~/breakpoints'; -// import waitForPromises from 'spec/helpers/wait_for_promises'; - -// describe('Job', () => { -// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; -// let mock; -// let response; -// let job; - -// preloadFixtures('builds/build-with-artifacts.html.raw'); - -// beforeEach(() => { -// loadFixtures('builds/build-with-artifacts.html.raw'); - -// spyOnDependency(Job, 'visitUrl'); - -// response = {}; - -// mock = new MockAdapter(axios); - -// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); -// }); - -// afterEach(() => { -// mock.restore(); - -// clearTimeout(job.timeout); -// }); - -// describe('class constructor', () => { -// beforeEach(() => { -// jasmine.clock().install(); -// }); - -// afterEach(() => { -// jasmine.clock().uninstall(); -// }); - -// describe('running build', () => { -// it('updates the build trace on an interval', function (done) { -// response = { -// html: '<span>Update<span>', -// status: 'running', -// state: 'newstate', -// append: true, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/Update/); -// expect(job.state).toBe('newstate'); - -// response = { -// html: '<span>More</span>', -// status: 'running', -// state: 'finalstate', -// append: true, -// complete: true, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); -// expect(job.state).toBe('finalstate'); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('replaces the entire build trace', (done) => { -// response = { -// html: '<span>Update<span>', -// status: 'running', -// append: false, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - -// response = { -// html: '<span>Different</span>', -// status: 'running', -// append: false, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); -// expect($('#build-trace .js-build-output').text()).toMatch(/Different/); -// }) -// .then(done) -// .catch(done.fail); -// }); -// }); - -// describe('truncated information', () => { -// describe('when size is less than total', () => { -// it('shows information about truncated log', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('shows the size in KiB', (done) => { -// const size = 50; - -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(size)}`); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('shows incremented size', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(50)}`); - -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: true, -// size: 10, -// total: 100, -// complete: true, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(60)}`); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('renders the raw link', () => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// expect( -// document.querySelector('.js-raw-link').textContent.trim(), -// ).toContain('Complete Raw'); -// }); -// }); - -// describe('when size is equal than total', () => { -// it('does not show the trunctated information', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 100, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); -// }) -// .then(done) -// .catch(done.fail); -// }); -// }); -// }); - -// describe('output trace', () => { -// beforeEach((done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(done) -// .catch(done.fail); -// }); - -// it('should render trace controls', () => { -// const controllers = document.querySelector('.controllers'); - -// expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull(); -// expect(controllers.querySelector('.js-scroll-up')).not.toBeNull(); -// expect(controllers.querySelector('.js-scroll-down')).not.toBeNull(); -// }); - -// it('should render received output', () => { -// expect( -// document.querySelector('.js-build-output').innerHTML, -// ).toEqual('<span>Update</span>'); -// }); -// }); -// }); - -// }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index e6d403dc826..ba1889c4dcd 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -52,7 +52,7 @@ describe('Job App ', () => { }); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {}); }); @@ -88,7 +88,9 @@ describe('Job App ', () => { describe('triggered job', () => { beforeEach(() => { - mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); + mock + .onGet(props.endpoint) + .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); vm = mountComponentWithStore(Component, { props, store }); }); @@ -133,57 +135,106 @@ describe('Job App ', () => { }); describe('stuck block', () => { - it('renders stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + describe('without active runners availabl', () => { + it('renders stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); - done(); - }, 0); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners that can run this job.", + ); + done(); + }, 0); + }); }); - it('renders tags in stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); + describe('when available runners can not run specified tag', () => { + it('renders tags in stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + }), + ); - vm = mountComponentWithStore(Component, { - props, - store, + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); }); + }); - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - done(); - }, 0); + describe('when runners are offline and build has tags', () => { + it('renders message about job being stuck because of no runners with the specified tags', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: true, + online: true, + }, + }), + ); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); + }); }); it('does not renders stuck block when there are no runners', done => { @@ -418,10 +469,11 @@ describe('Job App ', () => { vm.$store.state.trace = 'Update'; setTimeout(() => { - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update'); - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain( - 'Different', + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain( + 'Update', ); + + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different'); done(); }, 0); }); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 460a2e1b5da..424092d2d88 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -140,10 +140,11 @@ describe('Sidebar details block', () => { }); describe('while fetching stages', () => { - it('renders dropdown with More label', () => { + it('it does not render dropdown', () => { + store.dispatch('requestStages'); vm = mountComponentWithStore(SidebarComponent, { store }); - expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More'); + expect(vm.$el.querySelector('.js-selected-stage')).toBeNull(); }); }); diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index 45130b983e7..77b44995b12 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -68,41 +68,20 @@ describe('Job State actions', () => { describe('hideSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - hideSidebar, - null, - mockedState, - [{ type: types.HIDE_SIDEBAR }], - [], - done, - ); + testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done); }); }); describe('showSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - showSidebar, - null, - mockedState, - [{ type: types.SHOW_SIDEBAR }], - [], - done, - ); + testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done); }); }); describe('toggleSidebar', () => { describe('when isSidebarOpen is true', () => { it('should dispatch hideSidebar', done => { - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'hideSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done); }); }); @@ -110,14 +89,7 @@ describe('Job State actions', () => { it('should dispatch showSidebar', done => { mockedState.isSidebarOpen = false; - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'showSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done); }); }); }); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 46a20122ec8..4195d9d3680 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -175,43 +175,37 @@ describe('Job Store Getters', () => { }); }); - describe('isJobStuck', () => { - describe('when job is pending and runners are not available', () => { + describe('hasRunnersForProject', () => { + describe('with available and offline runners', () => { it('returns true', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: false, + available: true, + online: false, }; - expect(getters.isJobStuck(localState)).toEqual(true); + expect(getters.hasRunnersForProject(localState)).toEqual(true); }); }); - describe('when job is not pending', () => { + describe('with non available runners', () => { it('returns false', () => { - localState.job.status = { - group: 'running', - }; localState.job.runners = { available: false, + online: false, }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); - describe('when runners are available', () => { + describe('with online runners', () => { it('returns false', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: true, + available: false, + online: true, }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); }); diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js index 4230a7c42cf..d7908efcf13 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -124,8 +124,8 @@ describe('Jobs Store Mutations', () => { expect(stateCopy.job).toEqual({ id: 1312321 }); }); - it('sets selectedStage when the selectedStage is More', () => { - expect(stateCopy.selectedStage).toEqual('More'); + it('sets selectedStage when the selectedStage is empty', () => { + expect(stateCopy.selectedStage).toEqual(''); mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); expect(stateCopy.selectedStage).toEqual('deploy'); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 514d6ddeae5..0fb90c3b78c 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -35,9 +35,7 @@ describe('common_utils', () => { }); it('should decode params', () => { - expect( - commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0], - ).toBe('label_name[]=test'); + expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); }); it('should remove the question mark from the search params', () => { @@ -49,25 +47,19 @@ describe('common_utils', () => { describe('urlParamsToObject', () => { it('parses path for label with trailing +', () => { - expect( - commonUtils.urlParamsToObject('label_name[]=label%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ label_name: ['label+'], }); }); it('parses path for milestone with trailing +', () => { - expect( - commonUtils.urlParamsToObject('milestone_title=A%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ milestone_title: 'A+', }); }); it('parses path for search terms with spaces', () => { - expect( - commonUtils.urlParamsToObject('search=two+words', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({ search: 'two words', }); }); @@ -187,7 +179,11 @@ describe('common_utils', () => { describe('parseQueryStringIntoObject', () => { it('should return object with query parameters', () => { - expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ + scope: 'all', + page: '2', + }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); }); @@ -211,7 +207,9 @@ describe('common_utils', () => { describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); - expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual( + `${window.location.pathname}?page=2`, + ); }); }); @@ -266,21 +264,24 @@ describe('common_utils', () => { }); describe('normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + beforeEach(function() { + this.CLRFHeaders = + 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; spyOn(String.prototype, 'split').and.callThrough(); this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); }); - it('should split by newline', function () { + it('should split by newline', function() { expect(String.prototype.split).toHaveBeenCalledWith('\n'); }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + it('should split by colon+space for each header', function() { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe( + 3, + ); }); - it('should return a normalized headers object', function () { + it('should return a normalized headers object', function() { expect(this.normalizeCRLFHeaders).toEqual({ 'A-HEADER': 'a-value', 'ANOTHER-HEADER': 'ANOTHER-VALUE', @@ -359,67 +360,79 @@ describe('common_utils', () => { spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); }); - it('solves the promise from the callback', (done) => { + it('solves the promise from the callback', done => { const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + commonUtils + .backOff((next, stop) => + new Promise(resolve => { + resolve(expectedResponseValue); + }) + .then(resp => { + stop(resp); + }) + .catch(done.fail), + ) + .then(respBackoff => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }) - ).catch(done.fail)).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + .catch(done.fail); }); - it('catches the rejected promise from the callback ', (done) => { + it('catches the rejected promise from the callback ', done => { const errorMessage = 'Mistakes were made!'; - commonUtils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); - done(); - }); + commonUtils + .backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }) + .then(resp => { + stop(resp); + }) + .catch(err => stop(err)); + }) + .catch(errBackoffResp => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); + }); }); - it('solves the promise correctly after retrying a third time', (done) => { + it('solves the promise correctly after retrying a third time', done => { let numberOfCalls = 1; const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - ).catch(done.fail)).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + commonUtils + .backOff((next, stop) => + Promise.resolve(expectedResponseValue) + .then(resp => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } + }) + .catch(done.fail), + ) + .then(respBackoff => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); + }) + .catch(done.fail); }); - it('rejects the backOff promise after timing out', (done) => { - commonUtils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + it('rejects the backOff promise after timing out', done => { + commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); + done(); + }); }); }); @@ -466,11 +479,14 @@ describe('common_utils', () => { }); describe('createOverlayIcon', () => { - it('should return the favicon with the overlay', (done) => { - commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { - expect(url).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should return the favicon with the overlay', done => { + commonUtils + .createOverlayIcon(faviconDataUrl, overlayDataUrl) + .then(url => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); }); @@ -486,11 +502,16 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should set page favicon to provided favicon overlay', (done) => { - commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { - expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should set page favicon to provided favicon overlay', done => { + commonUtils + .setFaviconOverlay(overlayDataUrl) + .then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual( + faviconWithOverlayDataUrl, + ); + done(); + }) + .catch(done.fail); }); }); @@ -512,24 +533,24 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should reset favicon in case of error', (done) => { + it('should reset favicon in case of error', done => { mock.onGet(BUILD_URL).replyOnce(500); - commonUtils.setCiStatusFavicon(BUILD_URL) - .catch(() => { - const favicon = document.getElementById('favicon'); + commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => { + const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); - done(); - }); + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); + done(); + }); }); - it('should set page favicon to CI status favicon based on provided status', (done) => { + it('should set page favicon to CI status favicon based on provided status', done => { mock.onGet(BUILD_URL).reply(200, { favicon: overlayDataUrl, }); - commonUtils.setCiStatusFavicon(BUILD_URL) + commonUtils + .setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); @@ -554,11 +575,15 @@ describe('common_utils', () => { }); it('should return the svg for a linked icon', () => { - expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test')).toEqual( + '<svg ><use xlink:href="icons.svg#test" /></svg>', + ); }); it('should set svg className when passed', () => { - expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual( + '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>', + ); }); }); @@ -578,7 +603,7 @@ describe('common_utils', () => { const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); - Object.keys(convertedObj).forEach((prop) => { + Object.keys(convertedObj).forEach(prop => { expect(snakeRegEx.test(prop)).toBeFalsy(); expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); }); @@ -597,9 +622,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({ snakeKey: { child_snake_key: 'value', }, @@ -614,9 +637,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ snakeKey: { childSnakeKey: 'value', }, @@ -630,9 +651,7 @@ describe('common_utils', () => { }, ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ { childSnakeKey: 'value', }, @@ -648,9 +667,7 @@ describe('common_utils', () => { ], ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ [ { childSnakeKey: 'value', diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js deleted file mode 100644 index a9f3abcf2a4..00000000000 --- a/spec/javascripts/lib/utils/datefix_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { pad, pikadayToString } from '~/lib/utils/datefix'; - -describe('datefix', () => { - describe('pad', () => { - it('should add a 0 when length is smaller than 2', () => { - expect(pad(2)).toEqual('02'); - }); - - it('should not add a zero when lenght matches the default', () => { - expect(pad(12)).toEqual('12'); - }); - - it('should add a 0 when lenght is smaller than the provided', () => { - expect(pad(12, 3)).toEqual('012'); - }); - }); - - describe('parsePikadayDate', () => { - // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 - }); - - describe('pikadayToString', () => { - it('should format a UTC date into yyyy-mm-dd format', () => { - expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); - }); - }); -}); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index 9fedbcc4c25..d699e66b8ca 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -192,3 +192,181 @@ describe('formatTime', () => { }); }); }); + +describe('datefix', () => { + describe('pad', () => { + it('should add a 0 when length is smaller than 2', () => { + expect(datetimeUtility.pad(2)).toEqual('02'); + }); + + it('should not add a zero when length matches the default', () => { + expect(datetimeUtility.pad(12)).toEqual('12'); + }); + + it('should add a 0 when length is smaller than the provided', () => { + expect(datetimeUtility.pad(12, 3)).toEqual('012'); + }); + }); + + describe('parsePikadayDate', () => { + // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 + }); + + describe('pikadayToString', () => { + it('should format a UTC date into yyyy-mm-dd format', () => { + expect(datetimeUtility.pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); + }); + }); +}); + +describe('prettyTime methods', () => { + const assertTimeUnits = (obj, minutes, hours, days, weeks) => { + expect(obj.minutes).toBe(minutes); + expect(obj.hours).toBe(hours); + expect(obj.days).toBe(days); + expect(obj.weeks).toBe(weeks); + }; + + describe('parseSeconds', () => { + it('should correctly parse a negative value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(-1000); + + assertTimeUnits(zeroSeconds, 16, 0, 0, 0); + }); + + it('should correctly parse a zero value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(0); + + assertTimeUnits(zeroSeconds, 0, 0, 0, 0); + }); + + it('should correctly parse a small non-zero second values', () => { + const subOneMinute = datetimeUtility.parseSeconds(10); + const aboveOneMinute = datetimeUtility.parseSeconds(100); + const manyMinutes = datetimeUtility.parseSeconds(1000); + + assertTimeUnits(subOneMinute, 0, 0, 0, 0); + assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); + assertTimeUnits(manyMinutes, 16, 0, 0, 0); + }); + + it('should correctly parse large second values', () => { + const aboveOneHour = datetimeUtility.parseSeconds(4800); + const aboveOneDay = datetimeUtility.parseSeconds(110000); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); + }); + + it('should correctly accept a custom param for hoursPerDay', () => { + const config = { hoursPerDay: 24 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 1, 0); + assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); + }); + + it('should correctly accept a custom param for daysPerWeek', () => { + const config = { daysPerWeek: 7 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); + }); + + it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { + const config = { daysPerWeek: 55, hoursPerDay: 14 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 2, 2, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); + }); + }); + + describe('stringifyTime', () => { + it('should stringify values with all non-zero units', () => { + const timeObject = { + weeks: 1, + days: 4, + hours: 7, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('1w 4d 7h 20m'); + }); + + it('should stringify values with some non-zero units', () => { + const timeObject = { + weeks: 0, + days: 4, + hours: 0, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('4d 20m'); + }); + + it('should stringify values with no non-zero units', () => { + const timeObject = { + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('0m'); + }); + }); + + describe('abbreviateTime', () => { + it('should abbreviate stringified times for weeks', () => { + const fullTimeString = '1w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w'); + }); + + it('should abbreviate stringified times for non-weeks', () => { + const fullTimeString = '0w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d'); + }); + }); +}); + +describe('calculateRemainingMilliseconds', () => { + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + }); + + it('calculates the remaining time for a given end date', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-04T01:44:03Z'); + + expect(milliseconds).toBe(3723000); + }); + + it('returns 0 if the end date has passed', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-03T00:00:00Z'); + + expect(milliseconds).toBe(0); + }); +}); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index a5099a2a3b8..94c6214c86a 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,10 @@ -import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; +import { + formatRelevantDigits, + bytesToKiB, + bytesToMiB, + bytesToGiB, + numberToHumanSize, +} from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js index bb7a29fe30a..b9e805628f8 100644 --- a/spec/javascripts/lib/utils/text_markdown_spec.js +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -166,6 +166,33 @@ describe('init markdown', () => { expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select)); expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length); }); + + it('should support selected urls', () => { + const expectedUrl = 'http://www.gitlab.com'; + const expectedSelectionText = 'text'; + const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`; + const initialValue = `text ${expectedUrl} text`; + + textArea.value = initialValue; + const selectedIndex = initialValue.indexOf(expectedUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: expectedUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, + ); + }); }); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index ac3270baef5..92ebfc38722 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -120,7 +120,7 @@ describe('text_utility', () => { }); describe('getFirstCharacterCapitalized', () => { - it('returns the first character captialized, if first character is alphabetic', () => { + it('returns the first character capitalized, if first character is alphabetic', () => { expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L'); expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S'); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index a3477c5f8c6..565b87de248 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -113,6 +113,22 @@ describe('Dashboard', () => { }); }); + it('hides the dropdown list when there is no environments', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { ...propsData, hasMetrics: true, showPanels: false }, + }); + + component.store.storeEnvironmentsData([]); + + setTimeout(() => { + const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul'); + + expect(dropdownMenuEnvironments.length).toEqual(0); + done(); + }); + }); + it('renders the dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index a837b71db0b..038bfffd44f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; import { deploymentData } from '../mock_data'; -const createComponent = (propsData) => { +const createComponent = propsData => { const Component = Vue.extend(GraphFlag); return new Component({ @@ -51,8 +51,7 @@ describe('GraphFlag', () => { it('has a line at the currentXCoordinate', () => { component = createComponent(defaultValuesComponent); - expect(component.$el.style.left) - .toEqual(`${70 + component.currentXCoordinate}px`); + expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`); }); describe('Deployment flag', () => { @@ -62,9 +61,7 @@ describe('GraphFlag', () => { deploymentFlagData, }); - expect( - deploymentFlagComponent.$el.querySelector('.popover-title'), - ).toContainText('Deployed'); + expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed'); }); it('contains the ref when a tag is available', () => { @@ -78,13 +75,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + '1.0', + ); }); it('does not contain the ref when a tag is unavailable', () => { @@ -98,13 +95,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).not.toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText( + '1.0', + ); }); }); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 70dd5bb3be5..a81bdf618a3 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -11,11 +11,13 @@ describe('DiscussionFilter component', () => { beforeEach(() => { store = createStore(); - const discussions = [{ - ...discussionMock, - id: discussionMock.id, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], - }]; + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + }, + ]; const Component = Vue.extend(DiscussionFilter); const defaultValue = discussionFiltersMock[0].value; @@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => { }); it('renders the all filters', () => { - expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length); + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual( + discussionFiltersMock.length, + ); }); it('renders the default selected item', () => { - expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title); + expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual( + discussionFiltersMock[0].title, + ); }); it('updates to the selected item', () => { diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 06b30375306..3e289a6b8e6 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -97,7 +97,8 @@ describe('note_app', () => { }); it('should render list of notes', done => { - const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ + const note = + mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ '/gitlab-org/gitlab-ce/issues/26/discussions.json' ][0].notes[0]; diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 9d652ba9f1e..380ab59099d 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -78,7 +78,7 @@ describe('Notes Store mutations', () => { }); describe('COLLAPSE_DISCUSSION', () => { - it('should collpase an expanded discussion', () => { + it('should collapse an expanded discussion', () => { const discussion = Object.assign({}, discussionMock, { expanded: true }); const state = { diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index e21dca45fa1..f12950b8fce 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,7 +24,7 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); }); - it('should render emtpy state information', () => { + it('should render empty state information', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 027066e1d4d..3d2232ff239 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -50,7 +50,7 @@ describe('pipeline graph action component', () => { }); describe('on click', () => { - it('emits `pipelineActionRequestComplete` after a successfull request', done => { + it('emits `pipelineActionRequestComplete` after a successful request', done => { spyOn(component, '$emit'); component.$el.click(); diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index b6fa4272c8b..96a2d5f62fa 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -40,7 +40,9 @@ describe('graph component', () => { ).toEqual(true); expect( - component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), + component.$el + .querySelector('.stage-column:nth-child(2) .build:nth-child(1)') + .classList.contains('left-connector'), ).toEqual(true); expect(component.$el.querySelector('loading-icon')).toBe(null); @@ -56,7 +58,9 @@ describe('graph component', () => { pipeline: graphJSON, }); - expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + expect( + component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(), + ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); }); }); }); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index b5c62178642..a7dcd532f4f 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -62,9 +62,13 @@ describe('Pipelines Actions dropdown', () => { ); }; - beforeEach(() => { + beforeEach(done => { spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + + Vue.nextTick() + .then(done) + .catch(done.fail); }); it('emits postAction event after confirming', () => { diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 37908153e0e..97ded16db69 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -372,7 +372,7 @@ describe('Pipelines', () => { }); }); - describe('successfull request', () => { + describe('successful request', () => { describe('with pipelines', () => { beforeEach(() => { mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); @@ -667,7 +667,7 @@ describe('Pipelines', () => { }); }); - it('returns false when state is emtpy state', done => { + it('returns false when state is empty state', done => { vm.isLoading = false; vm.hasMadeRequest = true; vm.hasGitlabCi = false; diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index a3caaeb44dc..3c8b8032de8 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -40,7 +40,7 @@ describe('Pipelines stage component', () => { expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown'); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet('path.json').reply(200, stageReply); }); diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js deleted file mode 100644 index 158cd76dd13..00000000000 --- a/spec/javascripts/pretty_time_spec.js +++ /dev/null @@ -1,135 +0,0 @@ -import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time'; - -function assertTimeUnits(obj, minutes, hours, days, weeks) { - expect(obj.minutes).toBe(minutes); - expect(obj.hours).toBe(hours); - expect(obj.days).toBe(days); - expect(obj.weeks).toBe(weeks); -} - -describe('prettyTime methods', () => { - describe('parseSeconds', () => { - it('should correctly parse a negative value', () => { - const zeroSeconds = parseSeconds(-1000); - - assertTimeUnits(zeroSeconds, 16, 0, 0, 0); - }); - - it('should correctly parse a zero value', () => { - const zeroSeconds = parseSeconds(0); - - assertTimeUnits(zeroSeconds, 0, 0, 0, 0); - }); - - it('should correctly parse a small non-zero second values', () => { - const subOneMinute = parseSeconds(10); - const aboveOneMinute = parseSeconds(100); - const manyMinutes = parseSeconds(1000); - - assertTimeUnits(subOneMinute, 0, 0, 0, 0); - assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); - assertTimeUnits(manyMinutes, 16, 0, 0, 0); - }); - - it('should correctly parse large second values', () => { - const aboveOneHour = parseSeconds(4800); - const aboveOneDay = parseSeconds(110000); - const aboveOneWeek = parseSeconds(25000000); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); - }); - - it('should correctly accept a custom param for hoursPerDay', () => { - const config = { hoursPerDay: 24 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 1, 0); - assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); - }); - - it('should correctly accept a custom param for daysPerWeek', () => { - const config = { daysPerWeek: 7 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); - }); - - it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { - const config = { daysPerWeek: 55, hoursPerDay: 14 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 2, 2, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); - }); - }); - - describe('stringifyTime', () => { - it('should stringify values with all non-zero units', () => { - const timeObject = { - weeks: 1, - days: 4, - hours: 7, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('1w 4d 7h 20m'); - }); - - it('should stringify values with some non-zero units', () => { - const timeObject = { - weeks: 0, - days: 4, - hours: 0, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('4d 20m'); - }); - - it('should stringify values with no non-zero units', () => { - const timeObject = { - weeks: 0, - days: 0, - hours: 0, - minutes: 0, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('0m'); - }); - }); - - describe('abbreviateTime', () => { - it('should abbreviate stringified times for weeks', () => { - const fullTimeString = '1w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('1w'); - }); - - it('should abbreviate stringified times for non-weeks', () => { - const fullTimeString = '0w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('3d'); - }); - }); -}); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index e7f8f4f9936..eced4925489 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -78,9 +78,7 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: false, }, }).$mount(); @@ -90,7 +88,10 @@ describe('Assignee component', () => { expect(collapsed.childElementCount).toEqual(1); expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar); - expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`); + expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual( + `${UsersMock.user.name}'s avatar`, + ); + expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); }); @@ -98,34 +99,38 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); expect(component.$el.querySelector('.author-link')).not.toBeNull(); // The image - expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar); + expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual( + UsersMock.user.avatar, + ); // Author name - expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name); + expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual( + UsersMock.user.name, + ); // Username - expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`); + expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual( + `@${UsersMock.user.username}`, + ); }); it('has the root url present in the assigneeUrl method', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); - expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1); + expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual( + -1, + ); }); }); @@ -147,13 +152,19 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar); - expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`); + expect(second.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[1].name}'s avatar`, + ); + expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name); }); @@ -174,7 +185,10 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; @@ -196,7 +210,7 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); - it('Shows the "show-less" assignees label', (done) => { + it('Shows the "show-less" assignees label', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -206,21 +220,26 @@ describe('Assignee component', () => { }, }).$mount(); - expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount); + expect(component.$el.querySelectorAll('.user-item').length).toEqual( + component.defaultRenderCount, + ); + expect(component.$el.querySelector('.user-list-more')).not.toBe(null); const usersLabelExpectation = users.length - component.defaultRenderCount; - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .not.toBe(`+${usersLabelExpectation} more`); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe( + `+${usersLabelExpectation} more`, + ); component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); - it('Shows the "show-less" when "n+ more " label is clicked', (done) => { + it('Shows the "show-less" when "n+ more " label is clicked', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -232,8 +251,9 @@ describe('Assignee component', () => { component.$el.querySelector('.user-list-more .btn-link').click(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); @@ -264,16 +284,18 @@ describe('Assignee component', () => { }); it('shows "+1 more" label', () => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('+ 1 more'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '+ 1 more', + ); }); - it('shows "show less" label', (done) => { + it('shows "show less" label', done => { component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index ce850bc621e..3d44af11153 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -2,54 +2,48 @@ import Vue from 'vue'; import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import { getTimeago } from '~/lib/utils/datetime_utility'; +import mountComponent from '../../helpers/vue_mount_component_helper'; -const deploymentMockData = { - id: 15, - name: 'review/diplo', - url: '/root/acets-review-apps/environments/15', - stop_url: '/root/acets-review-apps/environments/15/stop', - metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', - external_url: 'http://diplo.', - external_url_formatted: 'diplo.', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], -}; -const createComponent = () => { +describe('Deployment component', () => { const Component = Vue.extend(deploymentComponent); + const deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + }; - return new Component({ - el: document.createElement('div'), - propsData: { deployment: { ...deploymentMockData } }, - }); -}; - -describe('Deployment component', () => { let vm; - beforeEach(() => { - vm = createComponent(); - }); - afterEach(() => { vm.$destroy(); }); - describe('computed', () => { + describe('', () => { + beforeEach(() => { + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); + }); + describe('deployTimeago', () => { it('return formatted date', () => { const readable = getTimeago().format(deploymentMockData.deployed_at); @@ -111,9 +105,7 @@ describe('Deployment component', () => { expect(vm.hasDeploymentMeta).toEqual(false); }); }); - }); - describe('methods', () => { describe('stopEnvironment', () => { const url = '/foo/bar'; const returnPromise = () => @@ -152,42 +144,33 @@ describe('Deployment component', () => { expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); }); }); - }); - - describe('template', () => { - let el; - - beforeEach(() => { - vm = createComponent(deploymentMockData); - el = vm.$el; - }); it('renders deployment name', () => { - expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( deploymentMockData.url, ); - expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); + expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); }); it('renders external URL', () => { - expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( deploymentMockData.external_url, ); - expect(el.querySelector('.js-deploy-url').innerText).toContain('View app'); + expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app'); }); it('renders stop button', () => { - expect(el.querySelector('.btn')).not.toBeNull(); + expect(vm.$el.querySelector('.btn')).not.toBeNull(); }); it('renders deployment time', () => { - expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); + expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); }); it('renders metrics component', () => { - expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull(); + expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull(); }); }); @@ -196,8 +179,7 @@ describe('Deployment component', () => { window.gon = window.gon || {}; window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = true; - - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -216,7 +198,7 @@ describe('Deployment component', () => { window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = false; - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -228,4 +210,64 @@ describe('Deployment component', () => { expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); }); }); + + describe('without changes', () => { + beforeEach(() => { + window.gon = window.gon || {}; + window.gon.features = window.gon.features || {}; + window.gon.features.ciEnvironmentsStatusChanges = true; + delete deploymentMockData.changes; + + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); + }); + + afterEach(() => { + delete window.gon.features.ciEnvironmentsStatusChanges; + }); + + it('renders the link to the review app without dropdown', () => { + expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); + expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); + }); + }); + + describe('deployment status', () => { + describe('running', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'running' }), + }); + }); + + it('renders information about running deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); + }); + }); + + describe('success', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'success' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to'); + }); + }); + + describe('failed', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'failed' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( + 'Failed to deploy to', + ); + }); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index ea8007d2029..6c7637eed13 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -22,6 +22,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, ciStatus: 'success', hasCi: true, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(true); @@ -30,6 +31,7 @@ describe('MRWidgetPipeline', () => { it('should return false when there is no pipeline', () => { vm = mountComponent(Component, { pipeline: {}, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(false); @@ -42,6 +44,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(false); @@ -52,6 +55,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(true); @@ -65,11 +69,12 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toEqual('Could not connect to the CI server. Please check your settings and try again'); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>', + ); }); describe('with a pipeline', () => { @@ -78,38 +83,41 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status and commit id', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link').textContent.trim(), - ).toEqual(mockData.pipeline.commit.short_id); + expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual( + mockData.pipeline.commit.short_id, + ); - expect( - vm.$el.querySelector('.js-commit-link').getAttribute('href'), - ).toEqual(mockData.pipeline.commit.commit_path); + expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual( + mockData.pipeline.commit.commit_path, + ); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -122,34 +130,35 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-commit-link')).toBeNull(); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -162,11 +171,10 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent, - ).not.toContain('Coverage'); + expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage'); }); }); @@ -179,6 +187,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js new file mode 100644 index 00000000000..68a65bd21c6 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import component from '~/vue_merge_request_widget/components/review_app_link.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('review app link', () => { + const Component = Vue.extend(component); + const props = { + link: '/review', + cssClass: 'js-link', + }; + let vm; + let el; + + beforeEach(() => { + vm = mountComponent(Component, props); + el = vm.$el; + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided link as href attribute', () => { + expect(el.getAttribute('href')).toEqual(props.link); + }); + + it('renders provided cssClass as class attribute', () => { + expect(el.getAttribute('class')).toEqual(props.cssClass); + }); + + it('renders View app text', () => { + expect(el.textContent.trim()).toEqual('View app'); + }); + + it('renders svg icon', () => { + expect(el.querySelector('svg')).not.toBeNull(); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index d68342635ef..da5cb752c6f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -69,7 +69,7 @@ describe('MRWidgetMerged', () => { expect(vm.shouldShowRemoveSourceBranch).toEqual(true); }); - it('returns false wehn sourceBranchRemoved is true', () => { + it('returns false when sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; expect(vm.shouldShowRemoveSourceBranch).toEqual(false); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 7fd1a2350f7..17554c4fe42 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -218,5 +218,7 @@ export default { diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', - merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + merge_commit_path: + 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + troubleshooting_docs_path: 'help', }; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index d1a064b9f4d..09fbe87b27e 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => { it('should fetch deployments', done => { spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }])); - vm.fetchDeployments(); + vm.fetchPreMergeDeployments(); setTimeout(() => { expect(vm.service.fetchDeployments).toHaveBeenCalled(); @@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, + status: 'success', }; beforeEach(done => { @@ -486,4 +487,192 @@ describe('mrWidgetOptions', () => { ).toEqual(changes.length); }); }); + + describe('pipeline for target branch after merge', () => { + describe('with information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + vm.mr.mergePipeline = { + id: 127, + user: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + active: true, + coverage: null, + source: 'push', + created_at: '2018-10-22T11:41:35.186Z', + updated_at: '2018-10-22T11:41:35.433Z', + path: '/root/ci-web-terminal/pipelines/127', + flags: { + latest: true, + stuck: true, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'test', + title: 'test: pending', + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + path: '/root/ci-web-terminal/pipelines/127#test', + dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test', + }, + ], + artifacts: [], + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'master', + path: '/root/ci-web-terminal/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'aa1939133d373c94879becb79d91828a892ee319', + short_id: 'aa193913', + title: "Merge branch 'master-test' into 'master'", + created_at: '2018-10-22T11:41:33.000Z', + parent_ids: [ + '4622f4dd792468993003caf2e3be978798cbe096', + '76598df914cdfe87132d0c3c40f80db9fa9396a4', + ], + message: + "Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1", + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2018-10-22T11:41:33.000Z', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2018-10-22T11:41:33.000Z', + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + }, + cancel_path: '/root/ci-web-terminal/pipelines/127/cancel', + }; + vm.$nextTick(done); + }); + + it('renders pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull(); + }); + + describe('with post merge deployments', () => { + beforeEach(done => { + vm.mr.postMergeDeployments = [ + { + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: + 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + status: 'success', + }, + ]; + + vm.$nextTick(done); + }); + + it('renders post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull(); + }); + }); + }); + + describe('without information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + }); + + describe('when state is not merged', () => { + beforeEach(done => { + vm.mr.state = 'archived'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + + it('does not render post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).toBeNull(); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js index 9914c0b70f3..67752c1c455 100644 --- a/spec/javascripts/vue_shared/components/file_row_spec.js +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -71,4 +71,40 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px'); }); + + describe('outputText', () => { + beforeEach(done => { + createComponent({ + file: { + ...file(), + path: 'app/assets/index.js', + }, + level: 0, + }); + + vm.displayTextKey = 'path'; + + vm.$nextTick(done); + }); + + it('returns text if truncateStart is 0', done => { + vm.truncateStart = 0; + + vm.$nextTick(() => { + expect(vm.outputText).toBe('app/assets/index.js'); + + done(); + }); + }); + + it('returns text truncated at start', done => { + vm.truncateStart = 5; + + vm.$nextTick(() => { + expect(vm.outputText).toBe('...ssets/index.js'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js index b71cb36ecf6..b84b5ae67a8 100644 --- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js +++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js @@ -41,7 +41,7 @@ describe('Filtered search dropdown', () => { }); }); - describe('when visible number is bigger than the items lenght', () => { + describe('when visible number is bigger than the items length', () => { beforeEach(() => { vm = mountComponent(Component, { items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }], diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js new file mode 100644 index 00000000000..929ffe219f4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/gl_countdown_spec.js @@ -0,0 +1,77 @@ +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import Vue from 'vue'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; + +describe('GlCountdown', () => { + const Component = Vue.extend(GlCountdown); + let vm; + let now = '2000-01-01T00:00:00Z'; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date(now).getTime()); + jasmine.clock().install(); + }); + + afterEach(() => { + vm.$destroy(); + jasmine.clock().uninstall(); + }); + + describe('when there is time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '2000-01-01T01:02:03Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays remaining time', () => { + expect(vm.$el).toContainText('01:02:03'); + }); + + it('updates remaining time', done => { + now = '2000-01-01T00:00:01Z'; + jasmine.clock().tick(1000); + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('01:02:02'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('when there is no time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '1900-01-01T00:00:00Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays 00:00:00', () => { + expect(vm.$el).toContainText('00:00:00'); + }); + }); + + describe('when an invalid date is passed', () => { + it('throws a validation error', () => { + spyOn(Vue.config, 'warnHandler').and.stub(); + vm = mountComponent(Component, { + endDateString: 'this is invalid', + }); + + expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1); + const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0); + + expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 026a0c7ea09..c507a97d37e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -11,29 +11,13 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('should render toggle sidebar if showToggleSidebar', (done) => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); - - vm.showToggleSidebar = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); - done(); - }); - }); - describe('toggleCollapse events', () => { - beforeEach((done) => { + beforeEach(done => { spyOn(vm, 'toggleSidebar'); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); - it('should emit when sidebar is toggled', () => { - vm.$el.querySelector('.gutter-toggle').click(); - - expect(vm.toggleSidebar).toHaveBeenCalled(); - }); - it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); @@ -42,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate and maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); @@ -58,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); @@ -72,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); }); @@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => { expect(icons.length).toEqual(1); expect(icons[0].innerText.trim()).toEqual('None'); }); + + it('should have tooltip as `Start and due date`', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + + expect(icons[0].dataset.originalTitle).toBe('Start and due date'); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js index 1581f4e3eb1..805ba7b9947 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); }); - it('should render date-picker when editing', (done) => { + it('should render date-picker when editing', done => { vm.editing = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.pika-label')).toBeDefined(); @@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => { }); describe('editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.editable = true; Vue.nextTick(done); }); @@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); }); - it('should enable editing when edit button is clicked', (done) => { + it('should enable editing when edit button is clicked', done => { vm.isLoading = false; Vue.nextTick(() => { vm.$el.querySelector('.title .btn-blank').click(); @@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => { }); }); - it('should render date if selectedDate', (done) => { + it('should render date if selectedDate', done => { vm.selectedDate = new Date('07/07/2017'); Vue.nextTick(() => { expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); @@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => { }); describe('selectedDate and editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.selectedDate = new Date('07/07/2017'); vm.editable = true; Vue.nextTick(done); @@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => { }); describe('showToggleSidebar', () => { - beforeEach((done) => { + beforeEach(done => { vm.showToggleSidebar = true; Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 9a691116cf8..804b33422bd 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => { const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + expect(vmMoreLabels.labelsList).toBe( + 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more', + ); vmMoreLabels.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 50b8d49d4bd..e022245d3ea 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -44,7 +44,7 @@ describe('User Avatar Link Component', function() { expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); }); - it('should return neccessary props as defined', function() { + it('should return necessary props as defined', function() { _.each(this.propsData, (val, key) => { expect(this.userAvatarLink[key]).toBeDefined(); }); diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js index 305d2fd5af4..1d516a280b0 100644 --- a/spec/javascripts/vue_shared/directives/tooltip_spec.js +++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js @@ -13,24 +13,45 @@ describe('Tooltip directive', () => { describe('with a single tooltip', () => { beforeEach(() => { - const SomeComponent = Vue.extend({ + setFixtures('<div id="dummy-element"></div>'); + vm = new Vue({ + el: '#dummy-element', directives: { tooltip, }, - template: ` - <div - v-tooltip - title="foo"> - </div> - `, + data() { + return { + tooltip: 'some text', + }; + }, + template: '<div v-tooltip :title="tooltip"></div>', }); - - vm = new SomeComponent().$mount(); }); it('should have tooltip plugin applied', () => { expect($(vm.$el).data('bs.tooltip')).toBeDefined(); }); + + it('displays the title as tooltip', () => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + expect(tooltipElement.innerText).toContain('some text'); + }); + + it('updates a visible tooltip', done => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + vm.tooltip = 'other text'; + + Vue.nextTick() + .then(() => { + expect(tooltipElement).toContainText('other text'); + done(); + }) + .catch(done.fail); + }); }); describe('with multiple tooltips', () => { diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb new file mode 100644 index 00000000000..41e6fb47b11 --- /dev/null +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe API::Helpers::CustomValidators do + let(:scope) do + Struct.new(:opts) do + def full_name(attr_name) + attr_name + end + end + end + + describe API::Helpers::CustomValidators::Absence do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'empty param' do + it 'does not raise a validation error' do + expect_no_validation_error({}) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_value' }) + end + end + end + + describe API::Helpers::CustomValidators::IntegerNoneAny do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => 2 }) + expect_no_validation_error({ 'test' => 100 }) + expect_no_validation_error({ 'test' => 'None' }) + expect_no_validation_error({ 'test' => 'Any' }) + expect_no_validation_error({ 'test' => 'none' }) + expect_no_validation_error({ 'test' => 'any' }) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_other_string' }) + end + end + end + + def expect_no_validation_error(params) + expect { validate_test_param!(params) }.not_to raise_error + end + + def expect_validation_error(params) + expect { validate_test_param!(params) }.to raise_error(Grape::Exceptions::Validation) + end + + def validate_test_param!(params) + subject.validate_param!('test', params) + end +end diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index a50329473ad..7a457403b51 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -76,7 +76,7 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end - it 'autolinks multiple occurences of smb' do + it 'autolinks multiple occurrences of smb' do link1 = 'smb:///Volumes/shared/foo.pdf' link2 = 'smb:///Volumes/shared/bar.pdf' diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index ed1ebe9ebf6..415ded05e6e 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -226,7 +226,7 @@ describe Banzai::Filter::RelativeLinkFilter do let(:ref) {'mark#\'@],+;-._/#@!$&()+down'} it 'correctly escapes the ref' do - # Adressable won't escape the '#', so we do this manually + # Addressable won't escape the '#', so we do this manually ref_escaped = 'mark%23\'@%5D,+;-._/%23@!$&()+down' # Stub this method so the branch doesn't actually need to be in the repo diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index c73faa55513..d3fff5bad42 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -64,7 +64,7 @@ describe ContainerRegistry::Blob do .to_return(status: 200) end - it 'returns true when blob has been successfuly deleted' do + it 'returns true when blob has been successfully deleted' do expect(blob.delete).to be_truthy end end diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index e1c4f9cfea7..5076996474f 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -118,7 +118,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat expect(fork_network_members.count).to eq(12) end - it 'knows when not all memberships withing a batch have been created' do + it 'knows when not all memberships within a batch have been created' do expect(migration.missing_members?(8, 10)).to be_truthy end end diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb new file mode 100644 index 00000000000..3e107ac3027 --- /dev/null +++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + subject { described_class.new } + + describe '#perform' do + context 'token is not yet hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + end + + it 'saves token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01'))) + end + + it 'erases token' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token }.from('token-01').to(nil)) + end + end + + context 'token is already hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01') + end + + it 'does not change existing token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token_digest }) + end + + it 'leaves token empty' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token }.from(nil)) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/redact_links_spec.rb b/spec/lib/gitlab/background_migration/redact_links_spec.rb new file mode 100644 index 00000000000..a40e68069cc --- /dev/null +++ b/spec/lib/gitlab/background_migration/redact_links_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::RedactLinks, :migration, schema: 20181014121030 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:merge_requests) { table(:merge_requests) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + def create_merge_request(id, params) + params.merge!(id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}") + + merge_requests.create(params) + end + + def create_issue(id, params) + params.merge!(id: id, title: "issue#{id}", project_id: project.id) + + issues.create(params) + end + + def create_note(id, params) + params[:id] = id + + notes.create(params) + end + + def create_snippet(id, params) + params.merge!(id: id, author_id: user.id) + + snippets.create(params) + end + + def create_resource(model, id, params) + send("create_#{model.name.underscore}", id, params) + end + + shared_examples_for 'redactable resource' do + it 'updates only matching texts' do + matching_text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + redacted_text = 'some text /sent_notifications/REDACTED/unsubscribe more text' + create_resource(model, 1, { field => matching_text }) + create_resource(model, 2, { field => 'not matching text' }) + create_resource(model, 3, { field => matching_text }) + create_resource(model, 4, { field => redacted_text }) + create_resource(model, 5, { field => matching_text }) + + expected = { field => 'some text /sent_notifications/REDACTED/unsubscribe more text', + "#{field}_html" => nil } + expect_any_instance_of("Gitlab::BackgroundMigration::RedactLinks::#{model}".constantize).to receive(:update_columns).with(expected).and_call_original + + subject.perform(model, field, 2, 4) + + expect(model.where(field => matching_text).pluck(:id)).to eq [1, 5] + expect(model.find(3).reload[field]).to eq redacted_text + end + end + + context 'resource is Issue' do + it_behaves_like 'redactable resource' do + let(:model) { Issue } + let(:field) { :description } + end + end + + context 'resource is Merge Request' do + it_behaves_like 'redactable resource' do + let(:model) { MergeRequest } + let(:field) { :description } + end + end + + context 'resource is Note' do + it_behaves_like 'redactable resource' do + let(:model) { Note } + let(:field) { :note } + end + end + + context 'resource is Snippet' do + it_behaves_like 'redactable resource' do + let(:model) { Snippet } + let(:field) { :description } + end + end +end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 4d5081b0a75..e5999a1c509 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do expect(pipeline_status.status).to eq(status) expect(pipeline_status.ref).to eq(ref) end + + context 'when status is empty string' do + before do + Gitlab::Redis::Cache.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: '', ref: ref }) + end + end + + it 'reads the status as nil' do + pipeline_status.load_from_cache + + expect(pipeline_status.status).to eq(nil) + end + end end describe '#has_cache?' do diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 4df426c54ae..81804ba5c76 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do let(:ref) { 'refs/heads/master' } let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } let(:protocol) { 'ssh' } + let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT } + let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) } subject(:change_access) do described_class.new( changes, project: project, user_access: user_access, - protocol: protocol + protocol: protocol, + logger: logger ) end @@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do end end + context 'when time limit was reached' do + it 'raises a TimeoutError' do + logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout) + access = described_class.new(changes, + project: project, + user_access: user_access, + protocol: protocol, + logger: logger) + + expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) + end + end + context 'when the user is not allowed to push to the repo' do it 'raises an error' do expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index ec22e3a198e..887ea8fc1e0 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Checks::LfsIntegrity do include ProjectForksHelper + let!(:time_left) { 50 } let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:newrev) do @@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects") end - subject { described_class.new(project, newrev) } + subject { described_class.new(project, newrev, time_left) } describe '#objects_missing?' do let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } @@ -67,7 +68,7 @@ describe Gitlab::Checks::LfsIntegrity do expect(subject.objects_missing?).to be_truthy end - it 'is false parent project already conatins LFS objects for the fork' do + it 'is false parent project already contains LFS objects for the fork' do lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object) diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb new file mode 100644 index 00000000000..0ed3940c038 --- /dev/null +++ b/spec/lib/gitlab/checks/timed_logger_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::TimedLogger do + let!(:timeout) { 50.seconds } + let!(:start) { Time.now } + let!(:ref) { "bar" } + let!(:logger) { described_class.new(start_time: start, timeout: timeout) } + let!(:log_messages) do + { + foo: "Foo message..." + } + end + + before do + logger.append_message("Checking ref: #{ref}") + end + + describe '#log_timed' do + it 'logs message' do + Timecop.freeze(start + 30.seconds) do + logger.log_timed(log_messages[:foo], start) { bar_check } + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)") + end + + context 'when time limit was reached' do + it 'cancels action' do + Timecop.freeze(start + 50.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + bar_check + end + end.to raise_error(described_class::TimeoutError) + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)") + end + + it 'cancels action with time elapsed if work was performed' do + Timecop.freeze(start + 30.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + grpc_check + end + end.to raise_error(described_class::TimeoutError) + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)") + end + end + end + end + + def bar_check + 2 + 2 + end + + def grpc_check + raise GRPC::DeadlineExceeded + end +end diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 7549e9941b6..5a5c071c639 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Ci::Ansi2html do expect(convert_html("Hello")).to eq('Hello') end - it "strips non-color-changing controll sequences" do + it "strips non-color-changing control sequences" do expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') end diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index 2ce858836e3..c2c0742efc3 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(policy).to be_satisfied_by(pipeline, seed) end - it 'is not satisfied by an overriden empty variable' do + it 'is not satisfied by an overridden empty variable' do policy = described_class.new(['$CI_PROJECT_NAME']) expect(policy).not_to be_satisfied_by(pipeline, seed) @@ -54,7 +54,7 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(policy).not_to be_satisfied_by(pipeline, seed) end - it 'allows to evaluate regular secret variables' do + it 'allows to evaluate regular CI variables' do create(:ci_variable, project: project, key: 'SECRET', value: 'my secret') policy = described_class.new(["$SECRET == 'my secret'"]) diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 1860ed79bfd..7c18514934e 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -219,7 +219,7 @@ describe Gitlab::Ci::Config::Entry::Global do ## # When nodes are specified but not defined, we assume that - # configuration is valid, and we asume that entry is simply undefined, + # configuration is valid, and we assume that entry is simply undefined, # despite the fact, that key is present. See issue #18775 for more # details. # diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb index bc9dbf2bece..239fa364f5e 100644 --- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_run?(dummy_controller)).to be_truthy end - it 'returns the the oposite of #should_skip? when the check is a skip' do + it 'returns the the opposite of #should_skip? when the check is a skip' do info = described_class.new({}, nil, nil, true) expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false) @@ -101,7 +101,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_skip?(dummy_controller)).to be_truthy end - it 'returns the the oposite of #should_run? when the check is not a skip' do + it 'returns the the opposite of #should_run? when the check is not a skip' do info = described_class.new({}, nil, nil, false) expect(info).to receive(:should_run?).with(dummy_controller).and_return(false) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index cc7cb3f23fd..248cca25a2c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete end describe "#remove_last_ocurrence" do - it "removes only the last occurance of a string" do + it "removes only the last occurrence of a string" do input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" expect(subject.remove_last_occurrence(input, "a-word-to-replace")) diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 2d94356f386..cc4faf6f10b 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -566,13 +566,13 @@ describe Gitlab::Diff::Position do end end - context "for text positon" do + context "for text position" do let(:args) { args_for_text } it_behaves_like "diff position json" end - context "for image positon" do + context "for image position" do let(:args) { args_for_img } it_behaves_like "diff position json" @@ -592,13 +592,13 @@ describe Gitlab::Diff::Position do end end - context "for text positon" do + context "for text position" do let(:args) { args_for_text } it_behaves_like "diff position json" end - context "for image positon" do + context "for image position" do let(:args) { args_for_img } it_behaves_like "diff position json" diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index ddc4f6c5b5c..a2eed07ca55 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Diff::PositionTracer do # # In any case, all of this means that the tests below will be extremely # (excessively, unjustifiably) thorough for scenarios where "the file was - # created in the old diff" and then drop off to comparitively lackluster + # created in the old diff" and then drop off to comparatively lackluster # testing of other scenarios. # # I did still try to cover most of the obvious and potentially tricky diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb index ca067a29174..134bd5657e7 100644 --- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::Git::AttributesAtRefParser, :seed_helper do end it 'handles missing blobs' do - expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error + expect { described_class.new(repository, 'non-existent-branch') }.not_to raise_error end describe '#attributes' do diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index 18ebfef38f0..f431d4e2a53 100644 --- a/spec/lib/gitlab/git/attributes_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do # It's a bit hard to test for something _not_ being processed. As such we'll # just test the number of entries. it 'ignores any comments and empty lines' do - expect(subject.patterns.length).to eq(10) + expect(subject.patterns.length).to eq(12) end end @@ -126,7 +126,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do describe '#each_line' do it 'iterates over every line in the attributes file' do - args = [String] * 14 # the number of lines in the file + args = [String] * 16 # the number of lines in the file expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index c5e7ab959b2..d035df7e0c2 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do it 'limits new_objects using object_limit' do expect(subject.new_pointers(object_limit: 1)).to eq([]) end + + it 'times out if given a small dynamic timeout' do + expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded) + end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 51eb997a325..9a443fa7f20 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1194,6 +1194,34 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#gitattribute' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') } + + after do + ensure_seeds + end + + it 'returns matching language attribute' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby') + end + + it 'returns matching language attribute with additional options' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json') + end + + it 'returns nil if nothing matches' do + expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil) + end + + context 'without gitattributes file' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + + it 'returns nil' do + expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil) + end + end + end + describe '#ref_exists?' do it 'returns true for an existing tag' do expect(repository.ref_exists?('refs/heads/master')).to eq(true) diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb new file mode 100644 index 00000000000..bcf4814edb6 --- /dev/null +++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::WrapsGitalyErrors do + subject(:wrapper) do + klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors } + klazz.new + end + + describe "#wrapped_gitaly_errors" do + mapping = { + GRPC::NotFound => Gitlab::Git::Repository::NoRepository, + GRPC::InvalidArgument => ArgumentError, + GRPC::BadStatus => Gitlab::Git::CommandError + } + + mapping.each do |grpc_error, error| + it "wraps #{grpc_error} in a #{error}" do + expect { wrapper.wrapped_gitaly_errors { raise grpc_error.new('wrapped') } } + .to raise_error(error) + end + end + + it 'does not swallow other errors' do + expect { wrapper.wrapped_gitaly_errors { raise 'raised' } } + .to raise_error(RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index e7da5565c26..a417ef77c9e 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -934,6 +934,16 @@ describe Gitlab::GitAccess do # There is still an N+1 query with protected branches expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1) end + + it 'raises TimeoutError when #check_single_change_access raises a timeout error' do + message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow" + + expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check| + expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError) + end + + expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message) + end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 47f37cae98f..39d09c49989 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -96,7 +96,7 @@ describe Gitlab::Gpg do expect(described_class.current_home_dir).to eq default_home_dir end - it 'returns the explicitely set home dir' do + it 'returns the explicitly set home dir' do GPGME::Engine.home_dir = '/tmp/gpg' expect(described_class.current_home_dir).to eq '/tmp/gpg' @@ -104,7 +104,7 @@ describe Gitlab::Gpg do GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir') end - it 'returns the default value when explicitely setting the home dir to nil' do + it 'returns the default value when explicitly setting the home dir to nil' do GPGME::Engine.home_dir = nil expect(described_class.current_home_dir).to eq default_home_dir diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 88f7099ff3c..fe0e9702f8a 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -5,44 +5,85 @@ describe Gitlab::Highlight do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:commit) { project.commit(sample_commit.id) } - - describe 'custom highlighting from .gitattributes' do - let(:branch) { 'gitattributes' } - let(:blob) { repository.blob_at_branch(branch, path) } + describe 'language provided' do let(:highlighter) do - described_class.new(blob.path, blob.data, repository: repository) + described_class.new('foo.erb', 'bar', language: 'erb?parent=json') end - before do - project.change_head('gitattributes') + it 'sets correct lexer' do + expect(highlighter.lexer.tag).to eq 'erb' + expect(highlighter.lexer.parent.tag).to eq 'json' end + end - describe 'basic language selection' do - let(:path) { 'custom-highlighting/test.gitlab-custom' } - it 'highlights as ruby' do - expect(highlighter.lexer.tag).to eq 'ruby' - end + describe '#highlight' do + let(:file_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end + + it 'highlights' do + expected = %Q[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> +<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>] + + expect(described_class.highlight(file_name, content)).to eq(expected) + end + + it 'returns plain version for unknown lexer context' do + result = described_class.highlight(file_name, no_context_content) + + expect(result).to eq(%[<span id="LC1" class="line" lang="">:type "assem"))</span>]) end - describe 'cgi options' do - let(:path) { 'custom-highlighting/test.gitlab-cgi' } + it 'returns plain version for long content' do + stub_const('Gitlab::Highlight::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1) + result = described_class.highlight(file_name, content) - it 'highlights as json with erb' do - expect(highlighter.lexer.tag).to eq 'erb' - expect(highlighter.lexer.parent.tag).to eq 'json' + expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span>]) + end + + it 'highlights multi-line comments' do + result = described_class.highlight(file_name, multiline_content) + html = Nokogiri::HTML(result) + lines = html.search('.s') + + expect(lines.count).to eq(3) + expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') + end + + context 'diff highlighting' do + let(:file_name) { 'test.diff' } + let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"} + let(:expected) do + %q(<span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> +<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> +<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span> +<span id="LC4" class="line" lang="diff"> ddd</span>) + end + + it 'highlights each line properly' do + result = described_class.highlight(file_name, content) + + expect(result).to eq(expected) end end - end - describe '#highlight' do describe 'with CRLF' do let(:branch) { 'crlf-diff' } let(:path) { 'files/whitespace' } let(:blob) { repository.blob_at_branch(branch, path) } let(:lines) do - described_class.highlight(blob.path, blob.data, repository: repository).lines + described_class.highlight(blob.path, blob.data).lines end it 'strips extra LFs' do diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index 0385dd762c2..1e583f4cee2 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -11,11 +11,8 @@ describe Gitlab::Identifier do describe '#identify' do context 'without an identifier' do - it 'identifies the user using a commit' do - expect(identifier).to receive(:identify_using_commit) - .with(project, '123') - - identifier.identify('', project, '123') + it 'returns nil' do + expect(identifier.identify('')).to be nil end end @@ -24,7 +21,7 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_user) .with("user-#{user.id}") - identifier.identify("user-#{user.id}", project, '123') + identifier.identify("user-#{user.id}") end end @@ -33,49 +30,11 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_ssh_key) .with("key-#{key.id}") - identifier.identify("key-#{key.id}", project, '123') + identifier.identify("key-#{key.id}") end end end - describe '#identify_using_commit' do - it "returns the User for an existing commit author's Email address" do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - - it 'returns nil when no user could be found' do - allow(project).to receive(:commit).with('123').and_return(nil) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'returns nil when the commit does not have an author Email' do - commit = double(:commit, author_email: nil) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'caches the found users per Email' do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').twice.and_return(commit) - - 2.times do - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - end - - it 'returns nil if the project & ref are not present' do - expect(identifier.identify_using_commit(nil, nil)).to be_nil - end - end - describe '#identify_using_user' do it 'returns the User for an existing ID in the identifier' do found = identifier.identify_using_user("user-#{user.id}") diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index b0570680d5a..365bfae0d88 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -321,7 +321,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end - context 'when the project has overriden params in import data' do + context 'when the project has overridden params in import data' do it 'overwrites the params stored in the JSON' do project.create_import_data(data: { override_params: { description: "Overridden" } }) diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 53c5a4e7c94..eed4135d8a2 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -6,104 +6,63 @@ describe Gitlab::Kubernetes::KubeClient do include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } - let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] } - let(:api_version) { 'v1' } let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } - let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) } + let(:client) { described_class.new(api_url, kubeclient_options) } before do stub_kubeclient_discover(api_url) end - describe '#hashed_clients' do - subject { client.hashed_clients } - - it 'has keys from api groups' do - expect(subject.keys).to match_array api_groups - end - - it 'has values of Kubeclient::Client' do - expect(subject.values).to all(be_an_instance_of Kubeclient::Client) - end - end - - describe '#clients' do - subject { client.clients } - - it 'is not empty' do - is_expected.to be_present - end - - it 'is an array of Kubeclient::Client objects' do - is_expected.to all(be_an_instance_of Kubeclient::Client) - end - - it 'has each API group url' do - expected_urls = api_groups.map { |group| "#{api_url}/#{group}" } - - expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls) + shared_examples 'a Kubeclient' do + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client end it 'has the kubeclient options' do - subject.each do |client| - expect(client.auth_options).to eq({ bearer_token: 'xyz' }) - end - end - - it 'has the api_version' do - subject.each do |client| - expect(client.instance_variable_get(:@api_version)).to eq('v1') - end + expect(subject.auth_options).to eq({ bearer_token: 'xyz' }) end end describe '#core_client' do subject { client.core_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the core API endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#rbac_client' do subject { client.rbac_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the RBAC API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#extensions_client' do subject { client.extensions_client } - let(:api_groups) { ['apis/extensions'] } - - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the extensions API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) end - end - describe '#discover!' do - it 'makes a discovery request for each API group' do - client.discover! - - api_groups.each do |api_group| - discovery_url = api_url + '/' + api_group + '/v1' - expect(WebMock).to have_requested(:get, discovery_url).once - end + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1') end end @@ -156,21 +115,12 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to method end - - context 'no rbac client' do - let(:api_groups) { ['api'] } - - it 'throws an error' do - expect { client.public_send(method) }.to raise_error(Module::DelegationError) - end - end end end end describe 'extensions API group' do let(:api_groups) { ['apis/extensions'] } - let(:api_version) { 'v1beta1' } let(:extensions_client) { client.extensions_client } describe '#get_deployments' do @@ -181,22 +131,11 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to :get_deployments end - - context 'no extensions client' do - let(:api_groups) { ['api'] } - let(:api_version) { 'v1' } - - it 'throws an error' do - expect { client.get_deployments }.to raise_error(Module::DelegationError) - end - end end end describe 'non-entity methods' do it 'does not proxy for non-entity methods' do - expect(client.clients.first).to respond_to :proxy_url - expect(client).not_to respond_to :proxy_url end @@ -211,14 +150,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:get_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe '#watch_pod_log' do @@ -227,14 +158,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:watch_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe 'methods that do not exist on any client' do diff --git a/spec/lib/gitlab/patch/draw_route_spec.rb b/spec/lib/gitlab/patch/draw_route_spec.rb new file mode 100644 index 00000000000..4009b903dc3 --- /dev/null +++ b/spec/lib/gitlab/patch/draw_route_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Patch::DrawRoute do + subject do + Class.new do + include Gitlab::Patch::DrawRoute + + def route_path(route_name) + File.expand_path("../../../../#{route_name}", __dir__) + end + end.new + end + + before do + allow(subject).to receive(:instance_eval) + end + + it 'evaluates CE only route' do + subject.draw(:help) + + expect(subject).to have_received(:instance_eval) + .with(File.read(subject.route_path('config/routes/help.rb'))) + .once + + expect(subject).to have_received(:instance_eval) + .once + end +end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 624e2add860..8df0facdab3 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true end + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + end + + it 'returns true for loopback IP' do + expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (0177.1)' do expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true end @@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do end end + it 'allows localhost endpoints' do + expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true) + end + + it 'allows loopback endpoints' do + expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true) + end + it 'allows IPv4 link-local endpoints' do expect(described_class).not_to be_blocked_url('http://169.254.169.254') expect(described_class).not_to be_blocked_url('http://169.254.168.100') @@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)]) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)]) end def unstub_domain_resolv diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb index 4eca53032a2..02c2fd47197 100644 --- a/spec/lib/gitlab/view/presenter/base_spec.rb +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::View::Presenter::Base do end end - context 'subject is overriden' do + context 'subject is overridden' do it 'returns true' do presenter = presenter_class.new(build_stubbed(:project, :public)) diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index c9756544bd6..2aaa7c24ad8 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -48,7 +48,7 @@ describe MicrosoftTeams::Notifier do stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {}) end - it 'expects to receive successfull answer' do + it 'expects to receive successful answer' do expect(subject.ping(options)).to be true end end diff --git a/spec/migrations/enqueue_redact_links_spec.rb b/spec/migrations/enqueue_redact_links_spec.rb new file mode 100644 index 00000000000..a5da76977b7 --- /dev/null +++ b/spec/migrations/enqueue_redact_links_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181014121030_enqueue_redact_links.rb') + +describe EnqueueRedactLinks, :migration, :sidekiq do + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + group = namespaces.create!(name: 'gitlab', path: 'gitlab') + project = projects.create!(namespace_id: group.id) + + merge_requests.create!(id: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master', description: text) + issues.create!(id: 1, description: text) + notes.create!(id: 1, note: text) + notes.create!(id: 2, note: text) + snippets.create!(id: 1, description: text, author_id: user.id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Note", "note", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, "Note", "note", 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Issue", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "MergeRequest", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Snippet", "description", 1, 1) + expect(BackgroundMigrationWorker.jobs.size).to eq 5 + end + end + end +end diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb index 4187ab149a5..af77d64fdbf 100644 --- a/spec/migrations/migrate_old_artifacts_spec.rb +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -76,7 +76,7 @@ describe MigrateOldArtifacts do end end - context 'when there are aritfacts in old and new directory' do + context 'when there are artifacts in old and new directory' do before do store_artifacts_in_legacy_path(build2) diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb new file mode 100644 index 00000000000..6d155f78342 --- /dev/null +++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb') + +describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 4) + + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02') + personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest') + personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04') + personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05') + personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + migrate! + + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5)) + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6)) + expect(BackgroundMigrationWorker.jobs.size).to eq 2 + end + end + + it 'schedules background migrations' do + perform_enqueued_jobs do + plain_text_token = 'token IS NOT NULL' + + expect(personal_access_tokens.where(plain_text_token).count).to eq 5 + + migrate! + + expect(personal_access_tokens.where(plain_text_token).count).to eq 0 + end + end +end diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb new file mode 100644 index 00000000000..59ad4e5417e --- /dev/null +++ b/spec/models/board_group_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardGroupRecentVisit do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:board) { create(:board, group: group) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:group) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, group: group) + board3 = create(:board, group: group) + + create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago + create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, group)).to eq recent + end + end +end diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb new file mode 100644 index 00000000000..275581945fa --- /dev/null +++ b/spec/models/board_project_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardProjectRecentVisit do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:board) { create(:board, project: project) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, project: project) + board3 = create(:board, project: project) + + create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago + create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, project)).to eq recent + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a046541031e..65e06f27f35 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2027,17 +2027,17 @@ describe Ci::Build do it { is_expected.to include(tag_variable) } end - context 'when secret variable is defined' do - let(:secret_variable) do + context 'when CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_variable, - secret_variable.slice(:key, :value).merge(project: project)) + ci_variable.slice(:key, :value).merge(project: project)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when protected variable is defined' do @@ -2072,17 +2072,17 @@ describe Ci::Build do end end - context 'when group secret variable is defined' do - let(:secret_variable) do + context 'when group CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_group_variable, - secret_variable.slice(:key, :value).merge(group: group)) + ci_variable.slice(:key, :value).merge(group: group)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when group protected variable is defined' do @@ -2357,7 +2357,7 @@ describe Ci::Build do .to receive(:predefined_variables) { [project_pre_var] } allow_any_instance_of(Project) - .to receive(:secret_variables_for) + .to receive(:ci_variables_for) .with(ref: 'master', environment: nil) do [create(:ci_variable, key: 'secret', value: 'value')] end @@ -2508,7 +2508,7 @@ describe Ci::Build do end describe '#scoped_variables_hash' do - context 'when overriding secret variables' do + context 'when overriding CI variables' do before do project.variables.create!(key: 'MY_VAR', value: 'my value 1') pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index eed298b433c..2473895a5d4 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' describe Clusters::Cluster do it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:cluster_projects) } it { is_expected.to have_many(:projects) } + it { is_expected.to have_many(:cluster_groups) } + it { is_expected.to have_many(:groups) } it { is_expected.to have_one(:provider_gcp) } it { is_expected.to have_one(:platform_kubernetes) } it { is_expected.to have_one(:application_helm) } @@ -178,6 +181,53 @@ describe Clusters::Cluster do it { expect(cluster.update(enabled: false)).to be_truthy } end end + + describe 'cluster_type validations' do + let(:instance_cluster) { create(:cluster, :instance) } + let(:group_cluster) { create(:cluster, :group) } + let(:project_cluster) { create(:cluster, :project) } + + it 'validates presence' do + cluster = build(:cluster, :project, cluster_type: nil) + + expect(cluster).not_to be_valid + expect(cluster.errors.full_messages).to include("Cluster type can't be blank") + end + + context 'project_type cluster' do + it 'does not allow setting group' do + project_cluster.groups << build(:group) + + expect(project_cluster).not_to be_valid + expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + end + + context 'group_type cluster' do + it 'does not allow setting project' do + group_cluster.projects << build(:project) + + expect(group_cluster).not_to be_valid + expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + + context 'instance_type cluster' do + it 'does not allow setting group' do + instance_cluster.groups << build(:group) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + + it 'does not allow setting project' do + instance_cluster.projects << build(:project) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + end end describe '#provider' do @@ -229,6 +279,23 @@ describe Clusters::Cluster do end end + describe '#group' do + subject { cluster.group } + + context 'when cluster belongs to a group' do + let(:cluster) { create(:cluster, :group) } + let(:group) { cluster.groups.first } + + it { is_expected.to eq(group) } + end + + context 'when cluster does not belong to any group' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end + describe '#applications' do set(:cluster) { create(:cluster) } diff --git a/spec/models/clusters/group_spec.rb b/spec/models/clusters/group_spec.rb new file mode 100644 index 00000000000..ba145342cb8 --- /dev/null +++ b/spec/models/clusters/group_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Group do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:group) } +end diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index 69083bdc125..debc02fa51f 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -24,13 +24,29 @@ describe Awardable do end end - describe ".awarded" do + describe "#awarded" do it "filters by user and emoji name" do expect(Issue.awarded(award_emoji.user, "thumbsup")).to be_empty expect(Issue.awarded(award_emoji.user, "thumbsdown")).to eq [issue] expect(Issue.awarded(award_emoji2.user, "thumbsup")).to eq [issue2] expect(Issue.awarded(award_emoji2.user, "thumbsdown")).to be_empty end + + it "filters by user and any emoji" do + issue3 = create(:issue) + create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user) + create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user) + + expect(Issue.awarded(award_emoji.user)).to eq [issue, issue3] + expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3] + end + end + + describe "#not_awarded" do + it "returns issues not awarded by user" do + expect(Issue.not_awarded(award_emoji.user)).to eq [issue2] + expect(Issue.not_awarded(award_emoji2.user)).to eq [issue] + end end end diff --git a/spec/models/concerns/blob_language_from_git_attributes_spec.rb b/spec/models/concerns/blob_language_from_git_attributes_spec.rb new file mode 100644 index 00000000000..7f05073b08e --- /dev/null +++ b/spec/models/concerns/blob_language_from_git_attributes_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BlobLanguageFromGitAttributes do + include FakeBlobHelpers + + let(:project) { build(:project, :repository) } + + describe '#language_from_gitattributes' do + subject(:blob) { fake_blob(path: 'file.md') } + + it 'returns return value from gitattribute' do + expect(blob.project.repository).to receive(:gitattribute).with(blob.path, 'gitlab-language').and_return('erb?parent=json') + + expect(blob.language_from_gitattributes).to eq('erb?parent=json') + end + + it 'returns nil if project is absent' do + allow(blob).to receive(:project).and_return(nil) + + expect(blob.language_from_gitattributes).to eq(nil) + end + end +end diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb index f8c2e29fadd..827fbc9d7d5 100644 --- a/spec/models/concerns/cacheable_attributes_spec.rb +++ b/spec/models/concerns/cacheable_attributes_spec.rb @@ -41,7 +41,7 @@ describe CacheableAttributes do expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last) end - it 'can be overriden' do + it 'can be overridden' do minimal_test_class.define_singleton_method(:current_without_cache) do first end @@ -64,7 +64,7 @@ describe CacheableAttributes do context 'with defaults defined' do include_context 'with defaults' - it 'can be overriden' do + it 'can be overridden' do expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' }) end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ec6374f3963..a4bf3e2350a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -519,7 +519,7 @@ describe Issuable do end end - context 'substracting time' do + context 'subtracting time' do before do spend_time(1800) end @@ -530,7 +530,7 @@ describe Issuable do expect(issue.total_time_spent).to eq(900) end - context 'when time to substract exceeds the total time spent' do + context 'when time to subtract exceeds the total time spent' do it 'raise a validation error' do Timecop.travel(1.minute.from_now) do expect do diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb new file mode 100644 index 00000000000..7d320edd492 --- /dev/null +++ b/spec/models/concerns/redactable_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Redactable do + shared_examples 'model with redactable field' do + it 'redacts unsubscribe token' do + model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + + model.save! + + expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text' + end + + it 'ignores not hexadecimal tokens' do + text = 'some text /sent_notifications/token/unsubscribe more text' + model[field] = text + + model.save! + + expect(model[field]).to eq text + end + + it 'ignores not matching texts' do + text = 'some text /sent_notifications/.*/unsubscribe more text' + model[field] = text + + model.save! + + expect(model[field]).to eq text + end + + it 'redacts the field when saving the model before creating markdown cache' do + model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + + model.save! + + expected = 'some text /sent_notifications/REDACTED/unsubscribe more text' + expect(model[field]).to eq expected + expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>" + end + end + + context 'when model is an issue' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:issue) } + let(:field) { :description } + end + end + + context 'when model is a merge request' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:merge_request) } + let(:field) { :description } + end + end + + context 'when model is a note' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:note) } + let(:field) { :note } + end + end + + context 'when model is a snippet' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:snippet) } + let(:field) { :description } + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 9b804429138..782687516ae 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -2,8 +2,6 @@ require 'spec_helper' shared_examples 'TokenAuthenticatable' do describe 'dynamically defined methods' do - it { expect(described_class).to be_private_method_defined(:generate_token) } - it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } it { is_expected.to respond_to("set_#{token_field}") } @@ -66,13 +64,275 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end describe 'multiple token fields' do - before do + before(:all) do described_class.send(:add_authentication_token_field, :yet_another_token) end - describe '.token_fields' do - subject { described_class.authentication_token_fields } - it { is_expected.to include(:runners_registration_token, :yet_another_token) } + it { is_expected.to respond_to(:ensure_runners_registration_token) } + it { is_expected.to respond_to(:ensure_yet_another_token) } + end + + describe 'setting same token field multiple times' do + subject { described_class.send(:add_authentication_token_field, :runners_registration_token) } + + it 'raises error' do + expect {subject}.to raise_error(ArgumentError) + end + end +end + +describe PersonalAccessToken, 'TokenAuthenticatable' do + let(:personal_access_token_name) { 'test-pat-01' } + let(:token_value) { 'token' } + let(:user) { create(:user) } + let(:personal_access_token) do + described_class.new(name: personal_access_token_name, + user_id: user.id, + scopes: [:api], + token: token, + token_digest: token_digest) + end + + before do + allow(Devise).to receive(:friendly_token).and_return(token_value) + end + + describe '.find_by_token' do + subject { PersonalAccessToken.find_by_token(token_value) } + + before do + personal_access_token.save + end + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'finds the token' do + expect(subject).not_to be_nil + expect(subject.name).to eql(personal_access_token_name) + end + end + + context 'token_digest does not exist' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'finds the token' do + expect(subject).not_to be_nil + expect(subject.name).to eql(personal_access_token_name) + end + end + end + + describe '#set_token' do + let(:new_token_value) { 'new-token' } + subject { personal_access_token.set_token(new_token_value) } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'overwrites token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'creates new token_digest and clears token' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + end + + describe '#ensure_token' do + subject { personal_access_token.ensure_token } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to be_nil + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to eql(token_value) + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to be_nil + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + end + + describe '#ensure_token!' do + subject { personal_access_token.ensure_token! } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to be_nil + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to eql(token_value) + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to be_nil + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + end + + describe '#reset_token!' do + subject { personal_access_token.reset_token! } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { 'old-token' } + let(:token_digest) { nil } + + it 'creates new token_digest and clears token' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest exists and newly generated token would be the same' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } + + before do + personal_access_token.save + allow(Devise).to receive(:friendly_token).and_return( + 'old-token', token_value, 'boom!') + end + + it 'regenerates a new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token exists and newly generated token would be the same' do + let(:token) { 'old-token' } + let(:token_digest) { nil } + + before do + personal_access_token.save + allow(Devise).to receive(:friendly_token).and_return( + 'old-token', token_value, 'boom!') + end + + it 'regenerates a new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c65e0b81451..1de95d881a7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -334,7 +334,7 @@ describe Environment do describe '#has_terminals?' do subject { environment.has_terminals? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do context 'and a deployment' do @@ -447,7 +447,7 @@ describe Environment do describe '#has_metrics?' do subject { environment.has_metrics? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do let(:project) { create(:prometheus_project) } @@ -456,8 +456,8 @@ describe Environment do it { is_expected.to be_truthy } end - context 'but no deployments' do - it { is_expected.to be_falsy } + context 'and no deployments' do + it { is_expected.to be_truthy } end end @@ -504,39 +504,6 @@ describe Environment do end end - describe '#has_metrics?' do - subject { environment.has_metrics? } - - context 'when the enviroment is available' do - context 'with a deployment service' do - let(:project) { create(:prometheus_project) } - - context 'and a deployment' do - let!(:deployment) { create(:deployment, environment: environment) } - it { is_expected.to be_truthy } - end - - context 'but no deployments' do - it { is_expected.to be_falsy } - end - end - - context 'without a monitoring service' do - it { is_expected.to be_falsy } - end - end - - context 'when the environment is unavailable' do - let(:project) { create(:prometheus_project) } - - before do - environment.stop - end - - it { is_expected.to be_falsy } - end - end - describe '#additional_metrics' do let(:project) { create(:prometheus_project) } subject { environment.additional_metrics } diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index f2eb263c98c..e7805d52d75 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -5,13 +5,15 @@ describe EnvironmentStatus do let(:environment) { deployment.environment} let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } + let(:sha) { deployment.sha } - subject(:environment_status) { described_class.new(environment, merge_request) } + subject(:environment_status) { described_class.new(environment, merge_request, sha) } it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } it { is_expected.to delegate_method(:project).to(:environment) } it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } + it { is_expected.to delegate_method(:status).to(:deployment) } describe '#project' do subject { environment_status.project } @@ -58,4 +60,32 @@ describe EnvironmentStatus do ) end end + + describe '.for_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + it 'is based on merge_request.head_pipeline' do + expect(merge_request).to receive(:head_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:merge_pipeline) + + described_class.for_merge_request(merge_request, admin) + end + end + + describe '.after_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + before do + merge_request.mark_as_merged! + end + + it 'is based on merge_request.merge_pipeline' do + expect(merge_request).to receive(:merge_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:head_pipeline) + + described_class.after_merge_request(merge_request, admin) + end + end end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index ab58f5c5021..b6355455c1d 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -92,41 +92,6 @@ describe GlobalMilestone do end end - describe '.states_count' do - context 'when the projects have milestones' do - before do - create(:closed_milestone, title: 'Active Group Milestone', project: project3) - create(:active_milestone, title: 'Active Group Milestone', project: project1) - create(:active_milestone, title: 'Active Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project1) - create(:closed_milestone, title: 'Closed Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project3) - end - - it 'returns the quantity of global milestones in each possible state' do - expected_count = { opened: 1, closed: 2, all: 2 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - - context 'when the projects do not have milestones' do - before do - project1 - end - - it 'returns 0 as the quantity of global milestones in each state' do - expected_count = { opened: 0, closed: 0, all: 0 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - end - describe '#initialize' do let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1bf8f89e126..ada00f03928 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -19,6 +19,8 @@ describe Group do it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:badges).class_name('GroupBadge') } + it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') } + it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') } describe '#members & #requesters' do let(:requester) { create(:user) } @@ -651,10 +653,10 @@ describe Group do end end - describe '#secret_variables_for' do + describe '#ci_variables_for' do let(:project) { create(:project, group: group) } - let!(:secret_variable) do + let!(:ci_variable) do create(:ci_group_variable, value: 'secret', group: group) end @@ -662,11 +664,11 @@ describe Group do create(:ci_group_variable, :protected, value: 'protected', group: group) end - subject { group.secret_variables_for('ref', project) } + subject { group.ci_variables_for('ref', project) } shared_examples 'ref is protected' do it 'contains all the variables' do - is_expected.to contain_exactly(secret_variable, protected_variable) + is_expected.to contain_exactly(ci_variable, protected_variable) end end @@ -676,8 +678,8 @@ describe Group do default_branch_protection: Gitlab::Access::PROTECTION_NONE) end - it 'contains only the secret variables' do - is_expected.to contain_exactly(secret_variable) + it 'contains only the CI variables' do + is_expected.to contain_exactly(ci_variable) end end @@ -710,9 +712,9 @@ describe Group do end it 'returns all variables belong to the group and parent groups' do - expected_array1 = [protected_variable, secret_variable] + expected_array1 = [protected_variable, ci_variable] expected_array2 = [variable_child, variable_child_2, variable_child_3] - got_array = group_child_3.secret_variables_for('ref', project).to_a + got_array = group_child_3.ci_variables_for('ref', project).to_a expect(got_array.shift(2)).to contain_exactly(*expected_array1) expect(got_array).to eq(expected_array2) diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index 6e35511e848..3f929710862 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -2,19 +2,13 @@ require 'spec_helper' describe LfsObject do describe '#local_store?' do - it 'returns true when file_store is nil' do - subject.file_store = nil - - expect(subject.local_store?).to eq true - end - it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do subject.file_store = LfsObjectUploader::Store::LOCAL expect(subject.local_store?).to eq true end - it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do + it 'returns false when file_store is equal to LfsObjectUploader::Store::REMOTE' do subject.file_store = LfsObjectUploader::Store::REMOTE expect(subject.local_store?).to eq false @@ -83,19 +77,6 @@ describe LfsObject do describe 'file is being stored' do let(:lfs_object) { create(:lfs_object, :with_file) } - context 'when object has nil store' do - before do - lfs_object.update_column(:file_store, nil) - lfs_object.reload - end - - it 'is stored locally' do - expect(lfs_object.file_store).to be(nil) - expect(lfs_object.file).to be_file_storage - expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) - end - end - context 'when existing object has local store' do it 'is stored locally' do expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 666d7e69f89..85a4ebac66c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -13,6 +13,20 @@ describe MergeRequest do it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:merge_request_diffs) } + + context 'for forks' do + let!(:project) { create(:project) } + let!(:fork) { fork_project(project) } + let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) } + + it 'does not load another project due to inverse relationship' do + expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id) + end + + it 'finds the associated merge request' do + expect(project.merge_requests.find(merge_request.id)).to eq(merge_request) + end + end end describe '#squash_in_progress?' do @@ -1058,6 +1072,26 @@ describe MergeRequest do end end + describe '#merge_pipeline' do + it 'returns nil when not merged' do + expect(subject.merge_pipeline).to be_nil + end + + context 'when the MR is merged' do + let(:sha) { subject.target_project.commit.id } + let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) } + + before do + subject.mark_as_merged! + subject.update_attribute(:merge_commit_sha, pipeline.sha) + end + + it 'returns the post-merge pipeline' do + expect(subject.merge_pipeline).to eq(pipeline) + end + end + end + describe '#has_ci?' do let(:merge_request) { build_stubbed(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 27d4e622710..d11eb46159e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -348,4 +348,41 @@ describe Milestone do end end end + + describe '.states_count' do + context 'when the projects have milestones' do + before do + project_1 = create(:project) + project_2 = create(:project) + group_1 = create(:group) + group_2 = create(:group) + + create(:active_milestone, title: 'Active Group Milestone', project: project_1) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_1) + create(:active_milestone, title: 'Active Group Milestone', project: project_2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_2) + create(:closed_milestone, title: 'Active Group Milestone', group: group_1) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_1) + create(:closed_milestone, title: 'Active Group Milestone', group: group_2) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_2) + end + + it 'returns the quantity of milestones in each possible state' do + expected_count = { opened: 5, closed: 6, all: 11 } + + count = described_class.states_count(Project.all, Group.all) + expect(count).to eq(expected_count) + end + end + + context 'when the projects do not have milestones' do + it 'returns 0 as the quantity of global milestones in each state' do + expected_count = { opened: 0, closed: 0, all: 0 } + + count = described_class.states_count([project]) + + expect(count).to eq(expected_count) + end + end + end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 2bb1c49b740..c82ab9c9e62 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -49,18 +49,36 @@ describe PersonalAccessToken do describe 'Redis storage' do let(:user_id) { 123 } - let(:token) { 'abc000foo' } + let(:token) { 'KS3wegQYXBLYhQsciwsj' } - before do - subject.redis_store!(user_id, token) + context 'reading encrypted data' do + before do + subject.redis_store!(user_id, token) + end + + it 'returns stored data' do + expect(subject.redis_getdel(user_id)).to eq(token) + end end - it 'returns stored data' do - expect(subject.redis_getdel(user_id)).to eq(token) + context 'reading unencrypted data' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set(described_class.redis_shared_state_key(user_id), + token, + ex: PersonalAccessToken::REDIS_EXPIRY_TIME) + end + end + + it 'returns stored data unmodified' do + expect(subject.redis_getdel(user_id)).to eq(token) + end end context 'after deletion' do before do + subject.redis_store!(user_id, token) + expect(subject.redis_getdel(user_id)).to eq(token) end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 0cd712e2f40..b0fd2ceead0 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -387,4 +387,22 @@ describe HipchatService do end end end + + context 'with UrlBlocker' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:hipchat) { described_class.new(project: project) } + let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + + describe '#execute' do + before do + hipchat.server = 'http://localhost:9123' + end + + it 'raises UrlBlocker for localhost' do + expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original + expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 62a38c66d99..d4b9a4c8cd6 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -88,6 +88,10 @@ describe Project do it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } + it 'has an inverse relationship with merge requests' do + expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) + end + context 'after initialized' do it "has a project_feature" do expect(described_class.new.project_feature).to be_present @@ -2432,10 +2436,10 @@ describe Project do end end - describe '#secret_variables_for' do + describe '#ci_variables_for' do let(:project) { create(:project) } - let!(:secret_variable) do + let!(:ci_variable) do create(:ci_variable, value: 'secret', project: project) end @@ -2443,7 +2447,7 @@ describe Project do create(:ci_variable, :protected, value: 'protected', project: project) end - subject { project.reload.secret_variables_for(ref: 'ref') } + subject { project.reload.ci_variables_for(ref: 'ref') } before do stub_application_setting( @@ -2452,13 +2456,13 @@ describe Project do shared_examples 'ref is protected' do it 'contains all the variables' do - is_expected.to contain_exactly(secret_variable, protected_variable) + is_expected.to contain_exactly(ci_variable, protected_variable) end end context 'when the ref is not protected' do - it 'contains only the secret variables' do - is_expected.to contain_exactly(secret_variable) + it 'contains only the CI variables' do + is_expected.to contain_exactly(ci_variable) end end @@ -2746,7 +2750,7 @@ describe Project do .to raise_error(ActiveRecord::RecordNotSaved, error_message) end - it 'updates the project succesfully' do + it 'updates the project successfully' do merge_request = create(:merge_request, target_project: project, source_project: project) expect { project.append_or_update_attribute(:merge_requests, [merge_request]) } @@ -3314,7 +3318,7 @@ describe Project do end end - context 'when explicitely enabled' do + context 'when explicitly enabled' do context 'when domain is empty' do before do create(:project_auto_devops, project: project, domain: nil) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f38fc191943..cc5e34782ec 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -145,7 +145,7 @@ describe ProjectWiki do end it "returns nil if the page does not exist" do - expect(subject.find_page("non-existant")).to eq(nil) + expect(subject.find_page("non-existent")).to eq(nil) end it "can find a page by slug" do @@ -226,7 +226,7 @@ describe ProjectWiki do end it 'returns nil if the page does not exist' do - expect(subject.find_file('non-existant')).to eq(nil) + expect(subject.find_file('non-existent')).to eq(nil) end it 'returns a Gitlab::Git::WikiFile instance' do diff --git a/spec/models/ssh_host_key_spec.rb b/spec/models/ssh_host_key_spec.rb new file mode 100644 index 00000000000..75db43b3d56 --- /dev/null +++ b/spec/models/ssh_host_key_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe SshHostKey do + using RSpec::Parameterized::TableSyntax + include ReactiveCachingHelpers + + let(:key1) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3UpyF2iLqy1d63M6k3jH1vuEnq/NWtE+o' \ + 'rJe1Xn7JoRbduKd6zpsJ0JhBGWgcQK0ph0aGW5PcudzzBSc+SlYfCc4GTaxDtmj41hW0o72m' \ + 'NiuDW3oKXXShOiVRde2ZOquH8Z865jGiZIC8BI/bXZD29IGUih0hPu7Rjp70VYiE+35QRf/p' \ + 'sD0Ddrz8QUIG3A/2dMzLI5F5ZORk3BIX2F3mJwJOvZxRhR/SqyphDMZ5eZ0EzqbFBCDE6HAB' \ + 'Woz9ck8RBGLvCIggmDHj3FmMLcQGMDiy6wKp7QdnBtxjCP6vtE6YPUM223AqsWt+9NTtCfB8' \ + 'YdNAH7YcHHOR1FgtSk1x' + end + + let(:key2) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLIp+4ciR2YO9f9rpldc7InNQw/TBUtcNb' \ + 'J2XR0rr15/5ytz7YM16xXG0Qjx576PNSmqs4gbTrvTuFZak+v1Jx/9deHRq/yqp9f+tv33+i' \ + 'aJGCQCX/+OVY7aWgV2R9YsS7XQ4mnv4XlOTEssib/rGAIT+ATd/GcdYSEOO+dh4O09/6O/jI' \ + 'MGSeP+NNetgn1nPCnLOjrXFZUnUtNDi6EEKeIlrliJjSb7Jr4f7gjvZnv4RskWHHFo8FgAAq' \ + 't0gOMT6EmKrnypBe2vLGSAXbtkXr01q6/DNPH+n9VA1LTV6v1KN/W5CN5tQV11wRSKiM8g5O' \ + 'Ebi86VjJRi2sOuYoXQU1' + end + + # Purposefully ordered so that `sort` will make changes + let(:known_hosts) do + <<~EOF + example.com #{key1} git@localhost + @revoked other.example.com #{key2} git@localhost + EOF + end + + let(:extra) { known_hosts + "foo\nbar\n" } + let(:reversed) { known_hosts.lines.reverse.join } + + let(:compare_host_keys) { nil } + + def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "") + stdin = StringIO.new + stdout = double(:stdout, read: stdout) + stderr = double(:stderr, read: stderr) + wait_thr = double(:wait_thr, value: double(success?: status)) + + expect(Open3).to receive(:popen3).with({}, 'ssh-keyscan', *args).and_yield(stdin, stdout, stderr, wait_thr) + + stdin + end + + let(:project) { build(:project) } + + subject(:ssh_host_key) { described_class.new(project: project, url: 'ssh://example.com:2222', compare_host_keys: compare_host_keys) } + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expected = [key1, key2] + .map { |data| Gitlab::SSHPublicKey.new(data) } + .each_with_index + .map { |key, i| { bits: key.bits, fingerprint: key.fingerprint, type: key.type, index: i } } + + expect(ssh_host_key.fingerprints.as_json).to eq(expected) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expect(ssh_host_key.fingerprints.as_json).to eq( + [ + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key1).fingerprint, type: :rsa, index: 0 }, + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key2).fingerprint, type: :rsa, index: 1 } + ] + ) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#host_keys_changed?' do + where(:known_hosts_a, :known_hosts_b, :result) do + known_hosts | extra | true + known_hosts | "foo\n" | true + known_hosts | '' | true + known_hosts | nil | true + known_hosts | known_hosts | false + reversed | known_hosts | false + extra | "foo\n" | true + '' | '' | false + nil | nil | false + '' | nil | false + end + + with_them do + let(:compare_host_keys) { known_hosts_b } + + subject { ssh_host_key.host_keys_changed? } + + context '(normal)' do + let(:compare_host_keys) { known_hosts_b } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_a) + end + + it { is_expected.to eq(result) } + end + + # Comparisons should be symmetrical, so test the reverse too + context '(reversed)' do + let(:compare_host_keys) { known_hosts_a } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_b) + end + + it { is_expected.to eq(result) } + end + end + end + + describe '#calculate_reactive_cache' do + subject(:cache) { ssh_host_key.calculate_reactive_cache } + + it 'writes the hostname to STDIN' do + stdin = stub_ssh_keyscan(%w[-T 5 -p 2222 -f-]) + + cache + + expect(stdin.string).to eq("example.com\n") + end + + context 'successful key scan' do + it 'stores the cleaned known_hosts data' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: "KEY 1\nKEY 1\n\n# comment\nKEY 2\n") + + is_expected.to eq(known_hosts: "KEY 1\nKEY 2\n") + end + end + + context 'failed key scan (exit code 1)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: 'blarg', status: false) + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + + context 'failed key scan (exit code 0)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stderr: 'Unknown host') + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + end +end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 36b8e5d304f..3c89e99abf0 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -91,7 +91,7 @@ describe Upload do .to change { upload.checksum }.from(nil).to(expected) end - it 'sets `checksum` to nil for a non-existant file' do + it 'sets `checksum` to nil for a non-existent file' do expect(upload).to receive(:exist?).and_return(false) checksum = Digest::SHA256.file(__FILE__).hexdigest diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b3474e74aa4..4e7c8523e65 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -730,6 +730,14 @@ describe User do expect(user.incoming_email_token).not_to be_blank end + + it 'uses SecureRandom to generate the incoming email token' do + expect(SecureRandom).to receive(:hex).and_return('3b8ca303') + + user = create(:user) + + expect(user.incoming_email_token).to eql('gitlab') + end end describe '#ensure_user_rights_and_limits' do diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb new file mode 100644 index 00000000000..e85e7a41017 --- /dev/null +++ b/spec/presenters/blob_presenter_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BlobPresenter, :seed_helper do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + + let(:git_blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/ruby/regex.rb' + ) + end + let(:blob) { Blob.new(git_blob) } + + describe '#highlight' do + subject { described_class.new(blob) } + + it 'returns highlighted content' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil) + + subject.highlight + end + + it 'returns plain content when :plain is true' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil) + + subject.highlight(plain: true) + end + + context 'gitlab-language contains a match' do + before do + allow(blob).to receive(:language_from_gitattributes).and_return('ruby') + end + + it 'passes language to inner call' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby') + + subject.highlight + end + end + end +end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index a1b52d8692d..bafcddebbb7 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -403,6 +403,15 @@ describe MergeRequestPresenter do is_expected .to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>") end + + it 'escapes html, when source_branch does not exist' do + xss_attempt = "<img src='x' onerror=alert('bad stuff') />" + + allow(resource).to receive(:source_branch) { xss_attempt } + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to eq(ERB::Util.html_escape(xss_attempt)) + end end describe '#rebase_path' do diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 3eb2f149311..7b0192fa9c8 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -239,7 +239,7 @@ describe ProjectPresenter do expect(presenter.new_file_anchor_data).to have_attributes(enabled: false, label: "New file", link: presenter.project_new_blob_path(project, 'master'), - class_modifier: 'new') + class_modifier: 'success') end it 'returns nil if user cannot push' do diff --git a/spec/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru new file mode 100644 index 00000000000..63daeb9eec5 --- /dev/null +++ b/spec/rack_servers/configs/config.ru @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +app = proc do |env| + if env['REQUEST_METHOD'] == 'GET' + [200, {}, ["#{Process.pid}"]] + else + Process.kill(env['QUERY_STRING'], Process.pid) + [200, {}, ['Bye!']] + end +end + +run app diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb new file mode 100644 index 00000000000..d6b6d83d648 --- /dev/null +++ b/spec/rack_servers/configs/puma.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only +# Note: as per the convention in `config/puma.example.development.rb`, +# this file will replace `/home/git` with the actual working directory + +directory '/home/git' +threads 1, 10 +queue_requests false +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +bind 'unix:///home/git/gitlab/tmp/tests/puma.socket' +workers 1 +preload_app! +worker_timeout 60 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +before_fork do + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + Gitlab::Cluster::LifecycleEvents.do_worker_start + File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid) +end + +on_restart do + Gitlab::Cluster::LifecycleEvents.do_master_restart +end diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb new file mode 100644 index 00000000000..431fab87857 --- /dev/null +++ b/spec/rack_servers/puma_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'fileutils' + +require 'excon' + +require 'spec_helper' + +describe 'Puma' do + before(:all) do + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('spec/rack_servers/configs/puma.rb') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + + config_path = File.join(project_root, "tmp/tests/puma.rb") + @socket_path = File.join(project_root, 'tmp/tests/puma.socket') + + File.write(config_path, config_lines) + + cmd = %W[puma -e test -C #{config_path} #{File.join(__dir__, 'configs/config.ru')}] + @puma_master_pid = spawn(*cmd) + wait_puma_boot!(@puma_master_pid, File.join(project_root, 'tmp/tests/puma-worker-ready')) + WebMock.allow_net_connect! + end + + %w[SIGQUIT SIGTERM SIGKILL].each do |signal| + it "has a worker that self-terminates on signal #{signal}" do + response = Excon.get('unix://', socket: @socket_path) + expect(response.status).to eq(200) + + worker_pid = response.body.to_i + expect(worker_pid).to be > 0 + + begin + Excon.post("unix://?#{signal}", socket: @socket_path) + rescue Excon::Error::Socket + # The connection may be closed abruptly + end + + expect(pid_gone?(worker_pid)).to eq(true) + end + end + + after(:all) do + begin + WebMock.disable_net_connect!(allow_localhost: true) + Process.kill('TERM', @puma_master_pid) + rescue Errno::ESRCH + end + end + + def wait_puma_boot!(master_pid, ready_file) + # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes. + timeout = 5 * 60 + timeout.times do + return if File.exist?(ready_file) + + pid = Process.waitpid(master_pid, Process::WNOHANG) + raise "puma failed to boot: #{$?}" unless pid.nil? + + sleep 1 + end + + raise "puma boot timed out after #{timeout} seconds" + end + + def pid_gone?(pid) + # Worker termination should take less than a second. That makes 10 + # seconds a generous timeout. + 10.times do + begin + Process.kill(0, pid) + rescue Errno::ESRCH + return true + end + + sleep 1 + end + + false + end +end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb index a4cf479a339..6a02ebcd048 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/rack_servers/unicorn_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fileutils' require 'excon' @@ -6,12 +8,16 @@ require 'spec_helper' describe 'Unicorn' do before(:all) do - config_lines = File.read('config/unicorn.rb.example').split("\n") + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('config/unicorn.rb.example') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + .split("\n") # Remove these because they make setup harder. config_lines = config_lines.reject do |line| %w[ - working_directory worker_processes listen pid @@ -26,33 +32,18 @@ describe 'Unicorn' do # predictable which process will handle our requests. config_lines << 'worker_processes 1' - @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket') + @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket') config_lines << "listen '#{@socket_path}'" - ready_file = 'tmp/tests/unicorn-worker-ready' + ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready') FileUtils.rm_f(ready_file) after_fork_index = config_lines.index { |l| l.start_with?('after_fork') } config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)") - config_path = 'tmp/tests/unicorn.rb' + config_path = File.join(project_root, 'tmp/tests/unicorn.rb') File.write(config_path, config_lines.join("\n") + "\n") - rackup_path = 'tmp/tests/config.ru' - File.write(rackup_path, <<~EOS) - app = - proc do |env| - if env['REQUEST_METHOD'] == 'GET' - [200, {}, [Process.pid]] - else - Process.kill(env['QUERY_STRING'], Process.pid) - [200, {}, ['Bye!']] - end - end - - run app - EOS - - cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}] + cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru] @unicorn_master_pid = spawn(*cmd) wait_unicorn_boot!(@unicorn_master_pid, ready_file) WebMock.allow_net_connect! diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 98399471f9a..2963dea634a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -565,7 +565,7 @@ describe API::Commits do } end - it 'are commited as one in project repo' do + it 'are committed as one in project repo' do post api(url, user), valid_mo_params expect(response).to have_gitlab_http_status(201) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index e0b5b34f9c4..5ea869796b0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -494,6 +494,24 @@ describe API::Internal do end end + context 'request times out' do + context 'git push' do + it 'responds with a gateway timeout' do + personal_project = create(:project, namespace: user.namespace) + + expect_next_instance_of(Gitlab::GitAccess) do |access| + expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo") + end + push(key, personal_project) + + expect(response).to have_gitlab_http_status(503) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq("Foo") + expect(user.reload.last_activity_on).to be_nil + end + end + end + context "archived project" do before do project.add_developer(user) @@ -665,7 +683,7 @@ describe API::Internal do expect(json_response).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end @@ -686,7 +704,7 @@ describe API::Internal do expect(json_response).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end @@ -819,7 +837,7 @@ describe API::Internal do expect(json_response['merge_request_urls']).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9f6cf12f9a7..3d532dd83c7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -55,8 +55,8 @@ describe API::Issues do end let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - let(:no_milestone_title) { URI.escape(Milestone::None.title) } - let(:any_milestone_title) { URI.escape(Milestone::Any.title) } + let(:no_milestone_title) { "None" } + let(:any_milestone_title) { "Any" } before(:all) do project.add_reporter(user) @@ -178,16 +178,44 @@ describe API::Issues do expect(first_issue['id']).to eq(issue2.id) end - it 'returns issues reacted by the authenticated user by the given emoji' do - issue2 = create(:issue, project: project, author: user, assignees: [user]) - award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star') + it 'returns issues with no assignee' do + issue2 = create(:issue, author: user2, project: project) - get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all' + get api('/issues', user), assignee_id: 'None', scope: 'all' expect_paginated_array_response(size: 1) expect(first_issue['id']).to eq(issue2.id) end + it 'returns issues with any assignee' do + # This issue without assignee should not be returned + create(:issue, author: user2, project: project) + + get api('/issues', user), assignee_id: 'Any', scope: 'all' + + expect_paginated_array_response(size: 3) + end + + it 'returns issues reacted by the authenticated user' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + create(:award_emoji, awardable: issue2, user: user2, name: 'star') + + create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup') + + get api('/issues', user2), my_reaction_emoji: 'Any', scope: 'all' + + expect_paginated_array_response(size: 2) + end + + it 'returns issues not reacted by the authenticated user' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + create(:award_emoji, awardable: issue2, user: user2, name: 'star') + + get api('/issues', user2), my_reaction_emoji: 'None', scope: 'all' + + expect_paginated_array_response(size: 2) + end + it 'returns issues matching given search string for title' do get api("/issues", user), search: issue.title @@ -1773,6 +1801,74 @@ describe API::Issues do end end + describe 'GET :id/issues/:issue_iid/related_merge_requests' do + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: "master", + target_branch: "test", + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + let!(:related_mr) { create_referencing_mr(user, project, issue) } + + context 'when unauthenticated' do + it 'return list of referenced merge requests from issue' do + get_related_merge_requests(project.id, issue.iid) + + expect_paginated_array_response(size: 1) + end + + it 'renders 404 if project is not visible' do + private_project = create(:project, :private) + private_issue = create(:issue, project: private_project) + create_referencing_mr(user, private_project, private_issue) + + get_related_merge_requests(private_project.id, private_issue.iid) + + expect(response).to have_gitlab_http_status(404) + end + end + + it 'returns merge requests that mentioned a issue' do + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "Some description") + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(related_mr.id) + end + + context 'no merge request mentioned a issue' do + it 'returns empty array' do + get_related_merge_requests(project.id, closed_issue.iid, user) + + expect_paginated_array_response(size: 0) + end + end + + it "returns 404 when issue doesn't exists" do + get_related_merge_requests(project.id, 999999, user) + + expect(response).to have_gitlab_http_status(404) + end + end + describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 07d19e3ad29..e4e0ca285e0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -143,6 +143,23 @@ describe API::MergeRequests do expect_response_ordered_exactly(merge_request3) end + it 'returns an array of merge requests with no assignee' do + merge_request3 = create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'None', scope: :all + + expect_response_ordered_exactly(merge_request3) + end + + it 'returns an array of merge requests with any assignee' do + # This MR with no assignee should not be returned + create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'Any', scope: :all + + expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked) + end + it 'returns an array of merge requests assigned to me' do merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 22e5a7c7174..62b6a3ce42e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -200,6 +200,24 @@ describe API::Projects do expect(json_response.first).to include 'statistics' end + it "does not include license by default" do + get api('/projects', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + + it "does not include license if requested" do + get api('/projects', user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + context 'when external issue tracker is enabled' do let!(:jira_service) { create(:jira_service, project: project) } @@ -994,6 +1012,26 @@ describe API::Projects do }) end + it "does not include license fields by default" do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include('license', 'license_url') + end + + it 'includes license fields when requested' do + get api("/projects/#{project.id}", user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['license']).to eq({ + 'key' => project.repository.license.key, + 'name' => project.repository.license.name, + 'nickname' => project.repository.license.nickname, + 'html_url' => project.repository.license.url, + 'source_url' => project.repository.license.meta['source'] + }) + end + it "does not include statistics by default" do get api("/projects/#{project.id}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 43ceb332cfb..909703a8d47 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -797,6 +797,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_runner_system_failure } end + + context 'when failure_reason is unrecognized value' do + before do + update_job(state: 'failed', failure_reason: 'what_is_this') + job.reload + end + + it { expect(job).to be_unknown_failure } + end + + context 'when failure_reason is job_execution_timeout' do + before do + update_job(state: 'failed', failure_reason: 'job_execution_timeout') + job.reload + end + + it { expect(job).to be_job_execution_timeout } + end end context 'when trace is given' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index c40d01e1a14..08bada44178 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -158,6 +158,16 @@ describe API::Wikis do expect(json_response.size).to eq(1) expect(json_response['error']).to eq('file is missing') end + + it 'responds with validation error on invalid temp file' do + payload[:file] = { tempfile: '/etc/hosts' } + + post(api(url, user), payload) + + expect(response).to have_gitlab_http_status(400) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is invalid') + end end describe 'GET /projects/:id/wikis' do diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 6894c65d639..1b4d8b70aa6 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } - let(:environment_status) { EnvironmentStatus.new(environment, merge_request) } + let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) } let(:entity) { described_class.new(environment_status, request: request) } subject { entity.as_json } @@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do it { is_expected.to include(:deployed_at) } it { is_expected.to include(:deployed_at_formatted) } it { is_expected.to include(:changes) } + it { is_expected.to include(:status) } it { is_expected.not_to include(:stop_url) } it { is_expected.not_to include(:metrics_url) } diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 5bf8aa7f23f..561421d5ac8 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do end end + describe 'merge_pipeline' do + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + + context 'when is merged' do + let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) } + + before do + project.add_maintainer(user) + end + + it 'returns merge_pipeline' do + pipeline.reload + pipeline_payload = PipelineDetailsEntity + .represent(pipeline, request: request) + .as_json + + expect(subject[:merge_pipeline]).to eq(pipeline_payload) + end + + context 'when user cannot read pipelines on target project' do + before do + project.add_guest(user) + end + + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + end + end + end + describe 'metrics' do context 'when metrics record exists with merged data' do before do diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb new file mode 100644 index 00000000000..6baf7ac9deb --- /dev/null +++ b/spec/services/boards/visits/create_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::CreateService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(project_board)).to eq nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(project_board)).to eq nil + end + + it 'records the visit' do + expect(BoardProjectRecentVisit).to receive(:visited!).once + + service.execute(project_board) + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(group_board)).to eq nil + end + + it 'records the visit' do + expect(BoardGroupRecentVisit).to receive(:visited!).once + + service.execute(group_board) + end + end + end +end diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb new file mode 100644 index 00000000000..e55d599e2cc --- /dev/null +++ b/spec/services/boards/visits/latest_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::LatestService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardProjectRecentVisit).to receive(:latest).once + + service.execute + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardGroupRecentVisit).to receive(:latest).once + + service.execute + end + end + end +end diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb index e2fa93904c5..55f123ee786 100644 --- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb +++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb @@ -24,7 +24,7 @@ describe Clusters::Gcp::FetchOperationService do end end - context 'when suceeded to fetch operation' do + context 'when succeeded to fetch operation' do before do stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id) end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 02983bea072..25363c54255 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -30,7 +30,7 @@ describe Clusters::Gcp::FinalizeCreationService do end end - context 'when suceeded to fetch gke cluster info' do + context 'when succeeded to fetch gke cluster info' do let(:endpoint) { '111.111.111.111' } let(:api_url) { 'https://' + endpoint } let(:username) { 'sample-username' } diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb index 065d021db5e..b096f1fa4fb 100644 --- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -16,7 +16,6 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index c543de21d5b..2355827fa5a 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -11,7 +11,6 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb index f48afdc83b2..c0bdac40938 100644 --- a/spec/services/clusters/gcp/provision_service_spec.rb +++ b/spec/services/clusters/gcp/provision_service_spec.rb @@ -26,7 +26,7 @@ describe Clusters::Gcp::ProvisionService do end end - context 'when suceeded to request provision' do + context 'when succeeded to request provision' do before do stub_cloud_platform_create_cluster(gcp_project_id, zone) end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index d71ccfb4334..dd8a1cee074 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -177,7 +177,7 @@ describe Groups::TransferService, :postgresql do it 'should add an error on group' do transfer_service.execute(new_parent_group) - expect(transfer_service.error).to eq('Transfer failed: Validation failed: Path has already been taken') + expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken') end end @@ -347,7 +347,7 @@ describe Groups::TransferService, :postgresql do end end - context 'when transfering a group with nested groups and projects' do + context 'when transferring a group with nested groups and projects' do let!(:group) { create(:group, :public) } let!(:project1) { create(:project, :repository, :private, namespace: group) } let!(:subgroup1) { create(:group, :private, parent: group) } diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 274624aa8bb..3e33a165e55 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } @@ -117,7 +117,7 @@ describe MergeRequests::GetUrlsService do let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch" } it 'returns 2 urls for both creating new and showing merge request' do result = service.execute(changes) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2536c6e2514..61c6ba7d550 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -306,6 +306,66 @@ describe MergeRequests::RefreshService do end end + context 'forked projects with the same source branch name as target branch' do + let!(:first_commit) do + @fork_project.repository.create_file(@user, 'test1.txt', 'Test data', + message: 'Test commit', + branch_name: 'master') + end + let!(:second_commit) do + @fork_project.repository.create_file(@user, 'test2.txt', 'More test data', + message: 'Second test commit', + branch_name: 'master') + end + let!(:forked_master_mr) do + create(:merge_request, + source_project: @fork_project, + source_branch: 'master', + target_branch: 'master', + target_project: @project) + end + let(:force_push_commit) { @project.commit('feature').id } + + it 'should reload a new diff for a push to the forked project' do + expect do + service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a force push to the source branch' do + expect do + service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a force push to the target branch' do + expect do + service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a push to the target project that contains a commit in the MR' do + expect do + service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should not increase the diff count for a new push to target branch' do + new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file', + message: 'This is a test', + branch_name: 'master') + + expect do + service.new(@project, @user).execute(@newrev, new_commit, 'refs/heads/master') + reload_mrs + end.not_to change { forked_master_mr.merge_request_diffs.count } + end + end + context 'push to origin repo target branch after fork project was removed' do before do @fork_project.destroy diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index e6ffa2b957b..06f865dc848 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -125,7 +125,7 @@ describe Projects::ImportService do project.import_type = 'bitbucket' end - it 'succeeds if repository import is successfull' do + it 'succeeds if repository import is successful' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({}) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1411723fb9e..2e07d4f8013 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -125,7 +125,7 @@ describe Projects::TransferService do it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' } end - context 'disallow transfering of project with tags' do + context 'disallow transferring of project with tags' do let(:container_repository) { create(:container_repository) } before do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 41a170e4f25..e513ee7ae44 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -315,7 +315,7 @@ describe QuickActions::InterpretService do end shared_examples 'award command' do - it 'toggle award 100 emoji if content containts /award :100:' do + it 'toggle award 100 emoji if content contains /award :100:' do _, updates = service.execute(content, issuable) expect(updates).to eq(emoji_award: "100") @@ -1395,7 +1395,7 @@ describe QuickActions::InterpretService do it 'includes the formatted duration and proper verb' do _, explanations = service.explain(content, issue) - expect(explanations).to eq(['Substracts 2h spent time.']) + expect(explanations).to eq(['Subtracts 2h spent time.']) end end diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb index 0d333d541c9..c76f6e6f77e 100644 --- a/spec/services/resource_events/merge_into_notes_service_spec.rb +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -66,5 +66,14 @@ describe ResourceEvents::MergeIntoNotesService do expect(notes.count).to eq 1 expect(notes.first.discussion_id).to eq event.discussion_id end + + it "preloads the note author's status" do + event = create_event(created_at: time) + create(:user_status, user: event.user) + + notes = described_class.new(resource, user).execute + + expect(notes.first.author.association(:status)).to be_loaded + end end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index c0ceb0f6605..18a7a392c12 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -100,7 +100,7 @@ RSpec.configure do |config| # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # but `block_and_wait_for_requests_complete` is called before it so by - # calling it explicitely here, we prevent any new requests from being fired + # calling it explicitly here, we prevent any new requests from being fired # See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25 # We don't reset the session when the example failed, because we need capybara-screenshot to have access to it. Capybara.reset_sessions! unless example.exception diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index f7f851eb1eb..bce1fb01355 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -5,7 +5,7 @@ shared_examples 'variable list' do end end - it 'adds new secret variable' do + it 'adds new CI variable' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') find('.js-ci-variable-input-value').set('key value') diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 5f42ff77fb2..6569feec39b 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -120,8 +120,12 @@ module FilteredSearchHelpers create_token('Label', label_name, symbol) end - def emoji_token(emoji_name = nil) - { name: 'My-Reaction', emoji_name: emoji_name } + def reaction_token(reaction_name = nil, is_emoji = true) + if is_emoji + { name: 'My-Reaction', emoji_name: reaction_name } + else + create_token('My-Reaction', reaction_name) + end end def default_placeholder diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb index 6a7132c3093..9a86560da2a 100644 --- a/spec/support/helpers/project_forks_helper.rb +++ b/spec/support/helpers/project_forks_helper.rb @@ -35,7 +35,7 @@ module ProjectForksHelper if create_repository # The call to project.repository.after_import in RepositoryForkWorker does # not reset the @exists variable of this forked_project.repository - # so we have to explicitely call this method to clear the @exists variable. + # so we have to explicitly call this method to clear the @exists variable. # of the instance we're returning here. forked_project.repository.after_import end diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb index 25781f5e679..90d7f60fdeb 100644 --- a/spec/support/helpers/seed_helper.rb +++ b/spec/support/helpers/seed_helper.rb @@ -1,15 +1,18 @@ +# frozen_string_literal: true + require_relative 'test_env' # This file is specific to specs in spec/lib/gitlab/git/ SEED_STORAGE_PATH = TestEnv.repos_path -TEST_REPO_PATH = 'gitlab-git-test.git'.freeze -TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'.freeze -TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze -TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze +TEST_REPO_PATH = 'gitlab-git-test.git' +TEST_NORMAL_REPO_PATH = 'not-bare-repo.git' +TEST_MUTABLE_REPO_PATH = 'mutable-repo.git' +TEST_BROKEN_REPO_PATH = 'broken-repo.git' +TEST_GITATTRIBUTES_REPO_PATH = 'with-git-attributes.git' module SeedHelper - GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze + GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__) def ensure_seeds if File.exist?(SEED_STORAGE_PATH) @@ -66,6 +69,11 @@ module SeedHelper end def create_git_attributes + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_GITATTRIBUTES_REPO_PATH}), + chdir: SEED_STORAGE_PATH, + out: '/dev/null', + err: '/dev/null') + dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info') FileUtils.mkdir_p(dir) @@ -82,6 +90,8 @@ foo/bar.* foo *.cgi key=value?p1=v1&p2=v2 /*.png gitlab-language=png *.binary binary +/custom-highlighting/*.gitlab-custom gitlab-language=ruby +/custom-highlighting/*.gitlab-cgi gitlab-language=erb?parent=json # This uses a tab instead of spaces to ensure the parser also supports this. *.md\tgitlab-language=markdown diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 71287f28171..71d72ff27e9 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -168,6 +168,8 @@ module TestEnv return end + FileUtils.mkdir_p("tmp/tests/second_storage") unless File.exist?("tmp/tests/second_storage") + spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s Bundler.with_original_env do raise "gitaly spawn failed" unless system(spawn_script) diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 7038a366144..9373de5aeab 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -17,10 +17,10 @@ RSpec.shared_examples 'a creatable merge request' do sign_in(user) visit project_new_merge_request_path( target_project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: source_project.id, target_project_id: target_project.id, - source_branch: 'fix', target_branch: 'master' }) end diff --git a/spec/support/shared_examples/services/boards/lists_move_service.rb b/spec/support/shared_examples/services/boards/lists_move_service.rb index 07c98cb29b7..2cdb968a45d 100644 --- a/spec/support/shared_examples/services/boards/lists_move_service.rb +++ b/spec/support/shared_examples/services/boards/lists_move_service.rb @@ -14,7 +14,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is equal to old position' do + it 'keeps position of lists when new position is equal to old position' do service = described_class.new(parent, user, position: planning.position) service.execute(planning) @@ -22,7 +22,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is negative' do + it 'keeps position of lists when new position is negative' do service = described_class.new(parent, user, position: -1) service.execute(planning) @@ -30,7 +30,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is equal to number of labels lists' do + it 'keeps position of lists when new position is equal to number of labels lists' do service = described_class.new(parent, user, position: board.lists.label.size) service.execute(planning) @@ -38,7 +38,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is greater than number of labels lists' do + it 'keeps position of lists when new position is greater than number of labels lists' do service = described_class.new(parent, user, position: board.lists.label.size + 1) service.execute(planning) @@ -46,7 +46,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'increments position of intermediate lists when new positon is equal to first position' do + it 'increments position of intermediate lists when new position is equal to first position' do service = described_class.new(parent, user, position: 0) service.execute(staging) @@ -54,7 +54,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [1, 2, 3, 0] end - it 'decrements position of intermediate lists when new positon is equal to last position' do + it 'decrements position of intermediate lists when new position is equal to last position' do service = described_class.new(parent, user, position: board.lists.label.last.position) service.execute(planning) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 3ba6caf1337..8c4360d4cf0 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -251,7 +251,7 @@ describe 'gitlab:app namespace rake task' do allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - # Avoid asking gitaly about the root ref (which will fail beacuse of the + # Avoid asking gitaly about the root ref (which will fail because of the # mocked storages) allow_any_instance_of(Repository).to receive(:empty?).and_return(false) end diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index aedbaa66d34..95f7f87d37b 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -29,6 +29,8 @@ describe 'projects/blob/_viewer.html.haml' do controller.params[:namespace_id] = project.namespace.to_param controller.params[:project_id] = project.to_param controller.params[:id] = File.join('master', blob.path) + + allow(project.repository).to receive(:gitattribute).and_return(nil) end def render_view diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index cd6661f09a1..9176eb12b12 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -6,7 +6,7 @@ describe PostReceive do let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } let(:gl_repository) { "project-#{project.id}" } let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + let!(:key_id) { key.shell_id } let(:project) do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') @@ -31,85 +31,108 @@ describe PostReceive do end describe "#process_project_changes" do - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + context 'empty changes' do + it "does not call any PushService but runs after project hooks" do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } + + described_class.new.perform(gl_repository, key_id, "") + end end - context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + context 'unidentified user' do + let!(:key_id) { "" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it 'returns false' do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false end end - context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + context 'with changes' do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "merge-requests" do - let(:changes) { "123456 789012 refs/merge-requests/123" } + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } - it "does not call any of the services" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "gitlab-ci.yml" do - let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + it "does not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end - context "creates a Ci::Pipeline for every change" do - before do - stub_ci_pipeline_to_return_yaml_file + context "gitlab-ci.yml" do + let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } - allow_any_instance_of(Project) - .to receive(:commit) - .and_return(project.commit) + subject { described_class.new.perform(gl_repository, key_id, base64_changes) } - allow_any_instance_of(Repository) - .to receive(:branch_exists?) - .and_return(true) - end + context "creates a Ci::Pipeline for every change" do + before do + stub_ci_pipeline_to_return_yaml_file - it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } - end + allow_any_instance_of(Project) + .to receive(:commit) + .and_return(project.commit) - context "does not create a Ci::Pipeline" do - before do - stub_ci_pipeline_yaml_file(nil) + allow_any_instance_of(Repository) + .to receive(:branch_exists?) + .and_return(true) + end + + it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } end - it { expect { subject }.not_to change { Ci::Pipeline.count } } + context "does not create a Ci::Pipeline" do + before do + stub_ci_pipeline_yaml_file(nil) + end + + it { expect { subject }.not_to change { Ci::Pipeline.count } } + end end - end - context 'after project changes hooks' do - let(:changes) { '123456 789012 refs/heads/tést' } - let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) - # silence hooks so we can isolate - allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) - end + before do + allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) + # silence hooks so we can isolate + allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) + allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end end end |