diff options
author | Jose Ivan Vargas <jvargas@gitlab.com> | 2018-02-06 00:16:41 +0300 |
---|---|---|
committer | Jose Ivan Vargas <jvargas@gitlab.com> | 2018-02-06 00:16:41 +0300 |
commit | 46ae03628de47d1bef2683a3a5fe4963b3df7d52 (patch) | |
tree | aca69b4acab10d5699a6315556de7e7530e65dbe /spec | |
parent | e6016d0bc2b640801914369e25e1a3639d3e50eb (diff) | |
parent | 2150ed4094ddb67d7b403cd56360700c80e7d928 (diff) |
Merge branch 'master' into jivl-update-katex
Diffstat (limited to 'spec')
991 files changed, 19449 insertions, 8905 deletions
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 2565622f8df..cc1b1e5039e 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -51,6 +51,13 @@ describe Admin::ApplicationSettingsController do sign_in(admin) end + it 'updates the password_authentication_enabled_for_git setting' do + put :update, application_setting: { password_authentication_enabled_for_git: "0" } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.password_authentication_enabled_for_git).to eq(false) + end + it 'updates the default_project_visibility for string value' do put :update, application_setting: { default_project_visibility: "20" } diff --git a/spec/controllers/admin/gitaly_servers_controller_spec.rb b/spec/controllers/admin/gitaly_servers_controller_spec.rb new file mode 100644 index 00000000000..b7378aa37d0 --- /dev/null +++ b/spec/controllers/admin/gitaly_servers_controller_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Admin::GitalyServersController do + describe '#index' do + before do + sign_in(create(:admin)) + end + + it 'shows the gitaly servers page' do + get :index + + expect(response).to have_gitlab_http_status(200) + end + end +end diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb index e6ba596117a..d2c1e634930 100644 --- a/spec/controllers/admin/hooks_controller_spec.rb +++ b/spec/controllers/admin/hooks_controller_spec.rb @@ -11,11 +11,13 @@ describe Admin::HooksController do it 'sets all parameters' do hook_params = { enable_ssl_verification: true, + token: "TEST TOKEN", + url: "http://example.com", + push_events: true, tag_push_events: true, repository_update_events: true, - token: "TEST TOKEN", - url: "http://example.com" + merge_requests_events: true } post :create, hook: hook_params diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index f044a068938..f350641a643 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -13,7 +13,7 @@ describe Admin::UsersController do let!(:issue) { create(:issue, author: user) } before do - project.team << [user, :developer] + project.add_developer(user) end it 'deletes user and ghosts their contributions' do diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 44d504d5852..79bbc29e80d 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -13,8 +13,8 @@ describe Boards::IssuesController do let!(:list2) { create(:list, board: board, label: development, position: 1) } before do - project.team << [user, :master] - project.team << [guest, :guest] + project.add_master(user) + project.add_guest(guest) end describe 'GET index' do @@ -221,7 +221,7 @@ describe Boards::IssuesController do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) end it 'returns a forbidden 403 response' do diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index a2b432af23a..71d45a22d91 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -7,8 +7,8 @@ describe Boards::ListsController do let(:guest) { create(:user) } before do - project.team << [user, :master] - project.team << [guest, :guest] + project.add_master(user) + project.add_guest(guest) end describe 'GET index' do diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb index fb9d3efbac0..7f2eaf95165 100644 --- a/spec/controllers/dashboard/groups_controller_spec.rb +++ b/spec/controllers/dashboard/groups_controller_spec.rb @@ -20,4 +20,24 @@ describe Dashboard::GroupsController do expect(assigns(:groups)).to contain_exactly(member_of_group) end + + context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do + let!(:top_level_result) { create(:group, name: 'chef-top') } + let!(:top_level_a) { create(:group, name: 'top-a') } + let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) } + let!(:other_group) { create(:group, name: 'other') } + + before do + top_level_result.add_master(user) + top_level_a.add_master(user) + end + + it 'renders only groups the user is a member of when searching hierarchy correctly' do + get :index, filter: 'chef', format: :json + + expect(response).to have_gitlab_http_status(200) + all_groups = [top_level_result, top_level_a, sub_level_result_a] + expect(assigns(:groups)).to contain_exactly(*all_groups) + end + end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 2f3d7be9abe..60547db82b6 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -17,7 +17,7 @@ describe Dashboard::MilestonesController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end it_behaves_like 'milestone tabs' diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index f9faa4fa59a..b4a731fd3a3 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -8,7 +8,7 @@ describe Dashboard::TodosController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end describe 'GET #index' do diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index 566d8515198..97c2c3fb940 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -5,7 +5,7 @@ describe DashboardController do let(:project) { create(:project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index cb1b460fc0e..22d3076c269 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -160,6 +160,30 @@ describe Groups::ChildrenController do expect(json_response).to eq([]) end + it 'succeeds if multiple pages contain matching subgroups' do + create(:group, parent: group, name: 'subgroup-filter-1') + create(:group, parent: group, name: 'subgroup-filter-2') + + # Creating the group-to-nest first so it would be loaded into the + # relation first before it's parents, this is what would cause the + # crash in: https://gitlab.com/gitlab-org/gitlab-ce/issues/40785. + # + # If we create the parent groups first, those would be loaded into the + # collection first, and the pagination would cut off the actual search + # result. In this case the hierarchy can be rendered without crashing, + # it's just incomplete. + group_to_nest = create(:group, parent: group, name: 'subsubgroup-filter-3') + subgroup = create(:group, parent: group) + 3.times do |i| + subgroup = create(:group, parent: subgroup) + end + group_to_nest.update!(parent: subgroup) + + get :index, group_id: group.to_param, filter: 'filter', per_page: 3, format: :json + + expect(response).to have_gitlab_http_status(200) + end + it 'includes pagination headers' do 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index c1aba46be04..733386500ca 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -28,7 +28,7 @@ describe Groups::MilestonesController do before do sign_in(user) group.add_owner(user) - project.team << [user, :master] + project.add_master(user) end describe '#index' do diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb index 67a11e56e94..6a1869d1a48 100644 --- a/spec/controllers/groups/uploads_controller_spec.rb +++ b/spec/controllers/groups/uploads_controller_spec.rb @@ -6,5 +6,7 @@ describe Groups::UploadsController do { group_id: model } end - it_behaves_like 'handle uploads' + it_behaves_like 'handle uploads' do + let(:uploader_class) { NamespaceFileUploader } + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a9cfd964dd5..492fed42d31 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -85,6 +85,30 @@ describe GroupsController do end end + describe 'GET #activity' do + render_views + + before do + sign_in(user) + project + end + + context 'as json' do + it 'includes all projects in event feed' do + 3.times do + project = create(:project, group: group) + create(:event, project: project) + end + + get :activity, id: group.to_param, format: :json + + expect(response).to have_gitlab_http_status(200) + expect(json_response['count']).to eq(3) + expect(assigns(:projects).limit_value).to be_nil + end + end + end + describe 'POST #create' do context 'when creating subgroups', :nested_groups do [true, false].each do |can_create_group_status| diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 2cead1770c9..387ca46ef6f 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -5,7 +5,7 @@ describe HealthCheckController do let(:json_response) { JSON.parse(response.body) } let(:xml_response) { Hash.from_xml(response.body)['hash'] } - let(:token) { current_application_settings.health_check_access_token } + let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } let(:not_whitelisted_ip) { '127.0.0.2' } diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index 95946def5f9..542eddc2d16 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -4,7 +4,7 @@ describe HealthController do include StubENV let(:json_response) { JSON.parse(response.body) } - let(:token) { current_application_settings.health_check_access_token } + let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } let(:not_whitelisted_ip) { '127.0.0.2' } diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb new file mode 100644 index 00000000000..8759d3c0b97 --- /dev/null +++ b/spec/controllers/import/gitlab_projects_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Import::GitlabProjectsController do + set(:namespace) { create(:namespace) } + set(:user) { namespace.owner } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + before do + sign_in(user) + end + + describe 'POST create' do + context 'with an invalid path' do + it 'redirects with an error' do + post :create, namespace_id: namespace.id, path: '/test', file: file + + expect(flash[:alert]).to start_with('Project could not be imported') + expect(response).to have_gitlab_http_status(302) + end + + it 'redirects with an error when a relative path is used' do + post :create, namespace_id: namespace.id, path: '../test', file: file + + expect(flash[:alert]).to start_with('Project could not be imported') + expect(response).to have_gitlab_http_status(302) + end + end + + context 'with a valid path' do + it 'redirects to the new project path' do + post :create, namespace_id: namespace.id, path: 'test', file: file + + expect(flash[:notice]).to include('is being imported') + expect(response).to have_gitlab_http_status(302) + end + end + end +end diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb index 9014b8b5084..e133950e684 100644 --- a/spec/controllers/notification_settings_controller_spec.rb +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -6,7 +6,7 @@ describe NotificationSettingsController do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) end describe '#create' do diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index b38652e7ab9..1195f44f37d 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -16,8 +16,7 @@ describe Oauth::ApplicationsController do end it 'redirects back to profile page if OAuth applications are disabled' do - settings = double(user_oauth_applications?: false) - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) get :index diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..c639ad32ec6 --- /dev/null +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe OmniauthCallbacksController do + include LoginHelpers + + let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: provider) } + let(:provider) { :github } + + before do + mock_auth_hash(provider.to_s, 'my-uid', user.email) + stub_omniauth_provider(provider, context: request) + end + + it 'allows sign in' do + post provider + + expect(request.env['warden']).to be_authenticated + end + + shared_context 'sign_up' do + let(:user) { double(email: 'new@example.com') } + + before do + stub_omniauth_setting(block_auto_created_users: false) + end + end + + context 'sign up' do + include_context 'sign_up' + + it 'is allowed' do + post provider + + expect(request.env['warden']).to be_authenticated + end + end + + context 'when OAuth is disabled' do + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + settings = Gitlab::CurrentSettings.current_application_settings + settings.update(disabled_oauth_sign_in_sources: [provider.to_s]) + end + + it 'prevents login via POST' do + post provider + + expect(request.env['warden']).not_to be_authenticated + end + + it 'shows warning when attempting login' do + post provider + + expect(response).to redirect_to new_user_session_path + expect(flash[:alert]).to eq('Signing in using GitHub has been disabled') + end + + it 'allows linking the disabled provider' do + user.identities.destroy_all + sign_in(user) + + expect { post provider }.to change { user.reload.identities.count }.by(1) + end + + context 'sign up' do + include_context 'sign_up' + + it 'is prevented' do + post provider + + expect(request.env['warden']).not_to be_authenticated + end + end + end +end diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index d1051741430..25a2e13fe1a 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::ArtifactsController do - set(:user) { create(:user) } + let(:user) { project.owner } set(:project) { create(:project, :repository, :public) } let(:pipeline) do @@ -15,14 +15,12 @@ describe Projects::ArtifactsController do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } before do - project.add_developer(user) - sign_in(user) end describe 'GET download' do it 'sends the artifacts file' do - expect(controller).to receive(:send_file).with(job.artifacts_file.path, disposition: 'attachment').and_call_original + expect(controller).to receive(:send_file).with(job.artifacts_file.path, hash_including(disposition: 'attachment')).and_call_original get :download, namespace_id: project.namespace, project_id: project, job_id: job end @@ -113,20 +111,42 @@ describe Projects::ArtifactsController do end describe 'GET raw' do + subject { get(:raw, namespace_id: project.namespace, project_id: project, job_id: job, path: path) } + context 'when the file exists' do - it 'serves the file using workhorse' do - get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt' + let(:path) { 'ci_artifacts.txt' } - send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER] + shared_examples 'a valid file' do + it 'serves the file using workhorse' do + subject - expect(send_data).to start_with('artifacts-entry:') + expect(response).to have_gitlab_http_status(200) + expect(send_data).to start_with('artifacts-entry:') - base64_params = send_data.sub(/\Aartifacts\-entry:/, '') - params = JSON.parse(Base64.urlsafe_decode64(base64_params)) + expect(params.keys).to eq(%w(Archive Entry)) + expect(params['Archive']).to start_with(archive_path) + # On object storage, the URL can end with a query string + expect(params['Archive']).to match(/build_artifacts.zip(\?[^?]+)?$/) + expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) + end + + def send_data + response.headers[Gitlab::Workhorse::SEND_DATA_HEADER] + end - expect(params.keys).to eq(%w(Archive Entry)) - expect(params['Archive']).to end_with('build_artifacts.zip') - expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) + def params + @params ||= begin + base64_params = send_data.sub(/\Aartifacts\-entry:/, '') + JSON.parse(Base64.urlsafe_decode64(base64_params)) + end + end + end + + context 'when using local file storage' do + it_behaves_like 'a valid file' do + let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + let(:archive_path) { JobArtifactUploader.root } + end end end end diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb index f5ea097af8b..6a41c4d23ea 100644 --- a/spec/controllers/projects/avatars_controller_spec.rb +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -1,15 +1,21 @@ require 'spec_helper' describe Projects::AvatarsController do - let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:project) { create(:project, :repository, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } let(:user) { create(:user) } before do sign_in(user) - project.team << [user, :master] + project.add_master(user) controller.instance_variable_set(:@project, project) end + it 'GET #show' do + get :show, namespace_id: project.namespace.id, project_id: project.id + + expect(response).to have_gitlab_http_status(404) + end + it 'removes avatar from DB by calling destroy' do delete :destroy, namespace_id: project.namespace.id, project_id: project.id expect(project.avatar.present?).to be_falsey diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb index 54282aa4001..88d4f4e9cd0 100644 --- a/spec/controllers/projects/blame_controller_spec.rb +++ b/spec/controllers/projects/blame_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::BlameController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) controller.instance_variable_set(:@project, project) end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 6a1c07b4a0b..00a7df6ccc8 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -89,7 +89,7 @@ describe Projects::BlobController do end before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -147,7 +147,7 @@ describe Projects::BlobController do let(:developer) { create(:user) } before do - project.team << [developer, :developer] + project.add_developer(developer) sign_in(developer) get :edit, default_params end @@ -161,7 +161,7 @@ describe Projects::BlobController do let(:master) { create(:user) } before do - project.team << [master, :master] + project.add_master(master) sign_in(master) get :edit, default_params end @@ -190,7 +190,7 @@ describe Projects::BlobController do end before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index d6ccb92c54b..4d765229bde 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::BoardsController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -55,6 +55,16 @@ describe Projects::BoardsController do end end + context 'issues are disabled' do + let(:project) { create(:project, :issues_disabled) } + + it 'returns a not found 404 response' do + list_boards + + expect(response).to have_gitlab_http_status(404) + end + end + def list_boards(format: :html) get :index, namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index d731200f70f..734396ddf7b 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -6,8 +6,8 @@ describe Projects::BranchesController do let(:developer) { create(:user) } before do - project.team << [user, :master] - project.team << [user, :developer] + project.add_master(user) + project.add_developer(user) allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) @@ -148,6 +148,20 @@ describe Projects::BranchesController do end end + context 'when create branch service fails' do + let(:branch) { "./invalid-branch-name" } + + it "doesn't post a system note" do + expect(SystemNoteService).not_to receive(:new_issue_branch) + + post :create, + namespace_id: project.namespace, + project_id: project, + branch_name: branch, + issue_iid: issue.iid + end + end + context 'without issue feature access' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb index 8b460646059..99fdff5f846 100644 --- a/spec/controllers/projects/clusters/applications_controller_spec.rb +++ b/spec/controllers/projects/clusters/applications_controller_spec.rb @@ -52,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do context 'when application is already installing' do before do - create(:cluster_applications_helm, :installing, cluster: cluster) + create(:clusters_applications_helm, :installing, cluster: cluster) end it 'returns 400' do diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index ee7928beb7e..775f9db1c6e 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -17,7 +17,6 @@ describe Projects::Clusters::GcpController do context 'when omniauth has been configured' do let(:key) { 'secret-key' } - let(:session_key_for_redirect_uri) do GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) end @@ -78,6 +77,8 @@ describe Projects::Clusters::GcpController do end it 'has new object' do + expect(controller).to receive(:authorize_google_project_billing) + go expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster) @@ -136,9 +137,16 @@ describe Projects::Clusters::GcpController do context 'when access token is valid' do before do stub_google_api_validate_token + allow_any_instance_of(described_class).to receive(:authorize_google_project_billing) end - context 'when creates a cluster on gke' do + context 'when google project billing is enabled' do + before do + redis_double = double + allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) + allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') + end + it 'creates a new cluster' do expect(ClusterProvisionWorker).to receive(:perform_async) expect { go }.to change { Clusters::Cluster.count } @@ -148,6 +156,15 @@ describe Projects::Clusters::GcpController do expect(project.clusters.first).to be_kubernetes end end + + context 'when google project billing is not enabled' do + it 'renders the cluster form with an error' do + go + + expect(response).to set_flash[:alert] + expect(response).to render_template('new') + end + end end context 'when access token is expired' do diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index c459d732507..55ed276f96b 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::CommitsController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe "GET show" do @@ -41,15 +41,21 @@ describe Projects::CommitsController do context "when the ref name ends in .atom" do context "when the ref does not exist with the suffix" do - it "renders as atom" do + before do get(:show, namespace_id: project.namespace, project_id: project, id: "master.atom") + end + it "renders as atom" do expect(response).to be_success expect(response.content_type).to eq('application/atom+xml') end + + it 'renders summary with type=html' do + expect(response.body).to include('<summary type="html">') + end end context "when the ref exists with the suffix" do diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index fe5818da0bc..046ce027965 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -8,7 +8,7 @@ describe Projects::CompareController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end it 'compare shows some diffs' do diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 6fae52edbad..7c708a418a7 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::CycleAnalyticsController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'cycle analytics not set up flag' do diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb index c3208357694..97db69427e9 100644 --- a/spec/controllers/projects/deploy_keys_controller_spec.rb +++ b/spec/controllers/projects/deploy_keys_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::DeployKeysController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -48,7 +48,7 @@ describe Projects::DeployKeysController do end before do - project2.team << [user, :developer] + project2.add_developer(user) end it 'returns json in a correct format' do diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index 3164fd5c143..73e7921fab7 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -8,7 +8,7 @@ describe Projects::DeploymentsController do let(:environment) { create(:environment, name: 'production', project: project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index 3bf676637a2..00328d3ea51 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -31,7 +31,7 @@ describe Projects::DiscussionsController do context "when the user is authorized to resolve the discussion" do before do - project.team << [user, :developer] + project.add_developer(user) end context "when the discussion is not resolvable" do @@ -92,7 +92,7 @@ describe Projects::DiscussionsController do context "when the user is authorized to resolve the discussion" do before do - project.team << [user, :developer] + project.add_developer(user) end context "when the discussion is not resolvable" do diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb index 6a5433bcc9c..505fe82851a 100644 --- a/spec/controllers/projects/find_file_controller_spec.rb +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::FindFileController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) controller.instance_variable_set(:@project, project) end diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 1bedb8ebdff..c4b32dc3a09 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -51,7 +51,7 @@ describe Projects::ForksController do context 'when user is a member of the Project' do before do - forked_project.team << [project.creator, :developer] + forked_project.add_developer(project.creator) end it 'sees the project listed' do diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index 5af03ae118c..c3605555fe7 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::GraphsController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'GET languages' do diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index f8c792cd0f0..5bfc3d31401 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::GroupLinksController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb index 07174660f46..2d473d5bf52 100644 --- a/spec/controllers/projects/hooks_controller_spec.rb +++ b/spec/controllers/projects/hooks_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::HooksController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -18,4 +18,30 @@ describe Projects::HooksController do ) end end + + describe '#create' do + it 'sets all parameters' do + hook_params = { + enable_ssl_verification: true, + token: "TEST TOKEN", + url: "http://example.com", + + push_events: true, + tag_push_events: true, + merge_requests_events: true, + issues_events: true, + confidential_issues_events: true, + note_events: true, + job_events: true, + pipeline_events: true, + wiki_page_events: true + } + + post :create, namespace_id: project.namespace, project_id: project, hook: hook_params + + expect(response).to have_http_status(302) + expect(ProjectHook.all.size).to eq(1) + expect(ProjectHook.first).to have_attributes(hook_params) + end + end end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 2a5ec6d584b..7fb4c1b7425 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -9,7 +9,7 @@ describe Projects::ImportsController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end it 'renders template' do @@ -30,7 +30,7 @@ describe Projects::ImportsController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end context 'when import is in progress' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index a2ef937609b..9656e7f7e74 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -37,7 +37,7 @@ describe Projects::IssuesController do context 'internal issue tracker' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like "issuables list meta-data", :issue @@ -69,7 +69,7 @@ describe Projects::IssuesController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) allow(Kaminari.config).to receive(:default_per_page).and_return(1) end @@ -102,6 +102,18 @@ describe Projects::IssuesController do expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) end + + it 'does not use pagination if disabled' do + allow(controller).to receive(:pagination_disabled?).and_return(true) + + get :index, + namespace_id: project.namespace.to_param, + project_id: project, + page: (last_page + 1).to_param + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:issues).size).to eq(2) + end end end @@ -116,7 +128,7 @@ describe Projects::IssuesController do context 'internal issue tracker' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it 'builds a new issue' do @@ -127,7 +139,7 @@ describe Projects::IssuesController do it 'fills in an issue for a merge request' do project_with_repository = create(:project, :repository) - project_with_repository.team << [user, :developer] + project_with_repository.add_developer(user) mr = create(:merge_request_with_diff_notes, source_project: project_with_repository) get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid @@ -153,7 +165,7 @@ describe Projects::IssuesController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) external = double allow(project).to receive(:external_issue_tracker).and_return(external) @@ -301,6 +313,53 @@ describe Projects::IssuesController do end end + describe 'GET #realtime_changes' do + def go(id:) + get :realtime_changes, + namespace_id: project.namespace.to_param, + project_id: project, + id: id + end + + context 'when an issue was edited' do + before do + project.add_developer(user) + + issue.update!(last_edited_by: user, last_edited_at: issue.created_at + 1.minute) + + sign_in(user) + end + + it 'returns last edited time' do + go(id: issue.iid) + + data = JSON.parse(response.body) + + expect(data).to include('updated_at') + expect(data['updated_at']).to eq(issue.last_edited_at.to_time.iso8601) + end + end + + context 'when an issue was edited by a deleted user' do + let(:deleted_user) { create(:user) } + + before do + project.add_developer(user) + + issue.update!(last_edited_by: deleted_user, last_edited_at: Time.now) + + deleted_user.destroy + sign_in(user) + end + + it 'returns 200' do + go(id: issue.iid) + + expect(response).to have_gitlab_http_status(200) + end + end + end + describe 'Confidential Issues' do let(:project) { create(:project_empty_repo, :public) } let(:assignee) { create(:assignee) } @@ -329,7 +388,7 @@ describe Projects::IssuesController do it 'does not list confidential issues for project members with guest role' do sign_in(member) - project.team << [member, :guest] + project.add_guest(member) get_issues @@ -354,7 +413,7 @@ describe Projects::IssuesController do it 'lists confidential issues for project members' do sign_in(member) - project.team << [member, :developer] + project.add_developer(member) get_issues @@ -394,7 +453,7 @@ describe Projects::IssuesController do it 'returns 404 for project members with guest role' do sign_in(member) - project.team << [member, :guest] + project.add_guest(member) go(id: unescaped_parameter_value.to_param) expect(response).to have_gitlab_http_status :not_found @@ -416,7 +475,7 @@ describe Projects::IssuesController do it "returns #{http_status[:success]} for project members" do sign_in(member) - project.team << [member, :developer] + project.add_developer(member) go(id: unescaped_parameter_value.to_param) expect(response).to have_gitlab_http_status http_status[:success] @@ -450,7 +509,7 @@ describe Projects::IssuesController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like 'restricted action', success: 200 @@ -589,25 +648,6 @@ describe Projects::IssuesController do project_id: project, id: id end - - context 'when an issue was edited by a deleted user' do - let(:deleted_user) { create(:user) } - - before do - project.team << [user, :developer] - - issue.update!(last_edited_by: deleted_user, last_edited_at: Time.now) - - deleted_user.destroy - sign_in(user) - end - - it 'returns 200' do - go(id: issue.iid) - - expect(response).to have_gitlab_http_status(200) - end - end end describe 'GET #edit' do @@ -638,7 +678,7 @@ describe Projects::IssuesController do def post_new_issue(issue_attrs = {}, additional_params = {}) sign_in(user) project = create(:project, :public) - project.team << [user, :developer] + project.add_developer(user) post :create, { namespace_id: project.namespace.to_param, @@ -655,7 +695,7 @@ describe Projects::IssuesController do let(:project) { merge_request.source_project } before do - project.team << [user, :master] + project.add_master(user) sign_in user end @@ -829,7 +869,7 @@ describe Projects::IssuesController do def post_spam admin = create(:admin) create(:user_agent_detail, subject: issue) - project.team << [admin, :master] + project.add_master(admin) sign_in(admin) post :mark_as_spam, { namespace_id: project.namespace, diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 7490f8fefce..db595430979 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -137,8 +137,8 @@ describe Projects::JobsController do it 'exposes needed information' do expect(response).to have_gitlab_http_status(:ok) - expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/) - expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/) + 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['new_issue_path']) .to include('/issues/new') end @@ -374,7 +374,7 @@ describe Projects::JobsController do let(:role) { :master } before do - project.team << [user, role] + project.add_role(user, role) sign_in(user) post_erase diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index cf83f2f3265..452d7e23983 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::LabelsController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index 33d48ff94d1..c5ac0be27bb 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::MattermostsController do let!(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb index 7fdddc02fd3..92db7284e0e 100644 --- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -4,27 +4,42 @@ describe Projects::MergeRequests::CreationsController do let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:fork_project) { create(:forked_project_with_submodules) } + let(:get_diff_params) do + { + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + } + end before do - fork_project.team << [user, :master] + fork_project.add_master(user) sign_in(user) end describe 'GET new' do context 'merge request that removes a submodule' do - render_views - it 'renders new merge request widget template' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + get :new, get_diff_params + + expect(response).to be_success + end + end + end + + describe 'GET diffs' do + context 'when merge request cannot be created' do + it 'does not assign diffs var' do + allow_any_instance_of(MergeRequest).to receive(:can_be_created).and_return(false) + + get :diffs, get_diff_params.merge(format: 'json') expect(response).to be_success + expect(assigns[:diffs]).to be_nil end end end @@ -37,14 +52,7 @@ describe Projects::MergeRequests::CreationsController do end it 'renders JSON including serialized pipelines' do - get :pipelines, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - }, - format: :json + get :pipelines, get_diff_params.merge(format: 'json') expect(response).to be_ok expect(json_response).to have_key 'pipelines' @@ -86,7 +94,7 @@ describe Projects::MergeRequests::CreationsController do let(:other_project) { create(:project, :repository) } before do - other_project.team << [user, :master] + other_project.add_master(user) end context 'when the path exists in the diff' do diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index ba97ccfbbd4..5d297c654bf 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -151,7 +151,7 @@ describe Projects::MergeRequests::DiffsController do let(:other_project) { create(:project) } before do - other_project.team << [user, :master] + other_project.add_master(user) diff_for_path(old_path: existing_path, new_path: existing_path, project_id: other_project) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 45c424af8c4..c8cc6b374f6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -684,4 +684,62 @@ describe Projects::MergeRequestsController do format: :json end end + + describe 'POST #rebase' do + let(:viewer) { user } + + def post_rebase + post :rebase, namespace_id: project.namespace, project_id: project, id: merge_request + end + + def expect_rebase_worker_for(user) + expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id) + end + + context 'successfully' do + it 'enqeues a RebaseWorker' do + expect_rebase_worker_for(viewer) + + post_rebase + + expect(response.status).to eq(200) + end + end + + context 'with a forked project' do + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:fork_owner) { fork_project.owner } + + before do + merge_request.update!(source_project: fork_project) + fork_project.add_reporter(user) + end + + context 'user cannot push to source branch' do + it 'returns 404' do + expect_rebase_worker_for(viewer).never + + post_rebase + + expect(response.status).to eq(404) + end + end + + context 'user can push to source branch' do + before do + project.add_reporter(fork_owner) + + sign_in(fork_owner) + end + + it 'returns 200' do + expect_rebase_worker_for(fork_owner) + + post_rebase + + expect(response.status).to eq(200) + end + end + end + end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 209979e642d..00cf464ec5b 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -11,7 +11,7 @@ describe Projects::MilestonesController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) controller.instance_variable_set(:@project, project) end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 37e9f863fc4..de132dfaa21 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -32,7 +32,7 @@ describe Projects::NotesController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it 'passes last_fetched_at from headers to NotesFinder' do @@ -351,7 +351,7 @@ describe Projects::NotesController do before do sign_in(note.author) - project.team << [note.author, :developer] + project.add_developer(note.author) end it "updates the note" do @@ -372,7 +372,7 @@ describe Projects::NotesController do context 'user is the author of a note' do before do sign_in(note.author) - project.team << [note.author, :developer] + project.add_developer(note.author) end it "returns status 200 for html" do @@ -389,7 +389,7 @@ describe Projects::NotesController do context 'user is not the author of a note' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it "returns status 404" do @@ -403,7 +403,7 @@ describe Projects::NotesController do describe 'POST toggle_award_emoji' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it "toggles the award emoji" do @@ -445,7 +445,7 @@ describe Projects::NotesController do context "when the user is authorized to resolve the note" do before do - project.team << [user, :developer] + project.add_developer(user) end context "when the note is not resolvable" do @@ -506,7 +506,7 @@ describe Projects::NotesController do context "when the user is authorized to resolve the note" do before do - project.team << [user, :developer] + project.add_developer(user) end context "when the note is not resolvable" do diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 290dba0610a..46b08a03b19 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -21,7 +21,7 @@ describe Projects::ProjectMembersController do context 'when user does not have enough rights' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'returns 404' do @@ -37,7 +37,7 @@ describe Projects::ProjectMembersController do context 'when user has enough rights' do before do - project.team << [user, :master] + project.add_master(user) end it 'adds user to members' do @@ -106,7 +106,7 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'returns 404' do @@ -121,7 +121,7 @@ describe Projects::ProjectMembersController do context 'when user has enough rights' do before do - project.team << [user, :master] + project.add_master(user) end it '[HTML] removes user from members' do @@ -164,7 +164,7 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'and is not an owner' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'removes user from members' do @@ -181,7 +181,7 @@ describe Projects::ProjectMembersController do let(:project) { create(:project, namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) end it 'cannot remove himself from the project' do @@ -248,7 +248,7 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'returns 404' do @@ -263,7 +263,7 @@ describe Projects::ProjectMembersController do context 'when user has enough rights' do before do - project.team << [user, :master] + project.add_master(user) end it 'adds user to members' do @@ -285,8 +285,8 @@ describe Projects::ProjectMembersController do let(:member) { create(:user) } before do - project.team << [user, :master] - another_project.team << [member, :guest] + project.add_master(user) + another_project.add_guest(member) sign_in(user) end @@ -300,7 +300,7 @@ describe Projects::ProjectMembersController do context 'when user can access source project members' do before do - another_project.team << [user, :guest] + another_project.add_guest(user) end include_context 'import applied' @@ -332,7 +332,7 @@ describe Projects::ProjectMembersController do context 'when creating owner' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -348,7 +348,7 @@ describe Projects::ProjectMembersController do context 'when create master' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 3a0c3faa7b4..b7df42168e0 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -53,7 +53,7 @@ describe Projects::RawController do end it 'serves the file' do - expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment') + expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment') get(:show, namespace_id: public_project.namespace.to_param, project_id: public_project, diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index 748ae040928..ceaffd92623 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::RefsController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end describe 'GET #logs_tree' do diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index 358f26dfb02..fc1619acec6 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::ReleasesController do let!(:tag) { release.tag } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 8b777eb68ca..04d16e98913 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -17,7 +17,7 @@ describe Projects::RepositoriesController do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index a907da2b60f..847ac6f2be0 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -9,7 +9,7 @@ describe Projects::ServicesController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe '#test' do @@ -114,5 +114,41 @@ describe Projects::ServicesController do expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.' end end + + context 'with a deprecated service' do + let(:service) { create(:kubernetes_service, project: project) } + + before do + put :update, + namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } + end + + it 'should not update the service' do + service.reload + expect(service.namespace).not_to eq('updated_namespace') + end + end + end + + describe "GET #edit" do + before do + get :edit, namespace_id: project.namespace, project_id: project, id: service_id + end + + context 'with approved services' do + let(:service_id) { 'jira' } + + it 'should render edit page' do + expect(response).to be_success + end + end + + context 'with a deprecated service' do + let(:service_id) { 'kubernetes' } + + it 'should render edit page' do + expect(response).to be_success + end + end end end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index b8fe0f46f57..0202149f335 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::Settings::CiCdController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -17,4 +17,51 @@ describe Projects::Settings::CiCdController do expect(response).to render_template(:show) end end + + describe '#reset_cache' do + before do + sign_in(user) + + project.add_master(user) + + allow(ResetProjectCacheService).to receive_message_chain(:new, :execute).and_return(true) + end + + subject { post :reset_cache, namespace_id: project.namespace, project_id: project } + + it 'calls reset project cache service' do + expect(ResetProjectCacheService).to receive_message_chain(:new, :execute) + + subject + end + + it 'redirects to project pipelines path' do + subject + + expect(response).to have_gitlab_http_status(:redirect) + expect(response).to redirect_to(project_pipelines_path(project)) + end + + context 'when service returns successfully' do + it 'sets the flash notice variable' do + subject + + expect(controller).to set_flash[:notice] + expect(controller).not_to set_flash[:error] + end + end + + context 'when service does not return successfully' do + before do + allow(ResetProjectCacheService).to receive_message_chain(:new, :execute).and_return(false) + end + + it 'sets the flash error variable' do + subject + + expect(controller).not_to set_flash[:notice] + expect(controller).to set_flash[:error] + end + end + end end diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb index 3068837f394..77df9a6f567 100644 --- a/spec/controllers/projects/settings/integrations_controller_spec.rb +++ b/spec/controllers/projects/settings/integrations_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::Settings::IntegrationsController do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 70e7f9ca96e..8fcfa3c9ecd 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -8,7 +8,7 @@ describe Projects::TemplatesController do let(:body) { JSON.parse(response.body) } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb index 4622e27e60f..1ce7e84bef9 100644 --- a/spec/controllers/projects/todos_controller_spec.rb +++ b/spec/controllers/projects/todos_controller_spec.rb @@ -20,7 +20,7 @@ describe Projects::TodosController do context 'when authorized' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it 'creates todo for issue' do @@ -36,7 +36,7 @@ describe Projects::TodosController do expect(response).to have_gitlab_http_status(200) expect(json_response['count']).to eq 1 - expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/) + expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}}) end end @@ -88,7 +88,7 @@ describe Projects::TodosController do context 'when authorized' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it 'creates todo for merge request' do @@ -104,7 +104,7 @@ describe Projects::TodosController do expect(response).to have_gitlab_http_status(200) expect(json_response['count']).to eq 1 - expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/) + expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}}) end end diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index 65b821c9486..d3188f054cf 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::TreeController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) controller.instance_variable_set(:@project, project) end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index d065cd00d00..9fde6544215 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::VariablesController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'POST #create' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e61187fb518..5202ffdd8bb 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -102,7 +102,7 @@ describe ProjectsController do render_views before do - project.team << [user, :developer] + project.add_developer(user) project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) end @@ -437,7 +437,7 @@ describe ProjectsController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) end @@ -465,7 +465,7 @@ describe ProjectsController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 7e42e43345c..376b229ffc9 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -180,6 +180,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response end end @@ -196,6 +197,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response end end @@ -220,6 +222,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response end end @@ -239,6 +242,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response end end @@ -265,13 +269,13 @@ describe UploadsController do context "when the user has access to the project" do before do - project.team << [user, :master] + project.add_master(user) end context "when the user is blocked" do before do user.block - project.team << [user, :master] + project.add_master(user) end it "redirects to the sign in page" do @@ -291,6 +295,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response end end @@ -322,6 +327,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response end end @@ -341,6 +347,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response end end @@ -384,6 +391,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response end end @@ -420,6 +428,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response end end @@ -439,6 +448,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response end end @@ -465,13 +475,13 @@ describe UploadsController do context "when the user has access to the project" do before do - project.team << [user, :master] + project.add_master(user) end context "when the user is blocked" do before do user.block - project.team << [user, :master] + project.add_master(user) end it "redirects to the sign in page" do @@ -491,6 +501,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response end end @@ -522,6 +533,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png' + response end end @@ -541,6 +553,7 @@ describe UploadsController do it_behaves_like 'content not cached without revalidation' do subject do get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png' + response end end diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb new file mode 100644 index 00000000000..48e2ff75cac --- /dev/null +++ b/spec/controllers/user_callouts_controller_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe UserCalloutsController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe "POST #create" do + subject { post :create, feature_name: feature_name, format: :json } + + context 'with valid feature name' do + let(:feature_name) { UserCallout.feature_names.keys.first } + + context 'when callout entry does not exist' do + it 'should create a callout entry with dismissed state' do + expect { subject }.to change { UserCallout.count }.by(1) + end + + it 'should return success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when callout entry already exists' do + let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) } + + it 'should return success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with invalid feature name' do + let(:feature_name) { 'bogus_feature_name' } + + it 'should return bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 01ab59aa363..2898c4b119e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -91,7 +91,7 @@ describe UsersController do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) push_data = Gitlab::DataBuilder::Push.build_sample(project, user) @@ -117,7 +117,7 @@ describe UsersController do allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it 'assigns @calendar_date' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index dc1d88c92dc..6f66468570f 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -7,12 +7,10 @@ FactoryBot.define do stage_idx 0 ref 'master' tag false - status 'pending' - created_at 'Di 29. Okt 09:50:00 CET 2013' - started_at 'Di 29. Okt 09:51:28 CET 2013' - finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' protected false + created_at 'Di 29. Okt 09:50:00 CET 2013' + pending options do { @@ -29,23 +27,37 @@ FactoryBot.define do pipeline factory: :ci_pipeline + trait :started do + started_at 'Di 29. Okt 09:51:28 CET 2013' + end + + trait :finished do + started + finished_at 'Di 29. Okt 09:53:28 CET 2013' + end + trait :success do + finished status 'success' end trait :failed do + finished status 'failed' end trait :canceled do + finished status 'canceled' end trait :skipped do + started status 'skipped' end trait :running do + started status 'running' end @@ -114,11 +126,6 @@ FactoryBot.define do build.project ||= build.pipeline.project end - factory :ci_not_started_build do - started_at nil - finished_at nil - end - trait :tag do tag true end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index d82fa8e34aa..775fbb3d27b 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :cluster_applications_helm, class: Clusters::Applications::Helm do + factory :clusters_applications_helm, class: Clusters::Applications::Helm do cluster factory: %i(cluster provided_by_gcp) trait :not_installable do @@ -31,5 +31,8 @@ FactoryBot.define do installing updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago end + + factory :clusters_applications_ingress, class: Clusters::Applications::Ingress + factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus end end diff --git a/spec/factories/clusters/applications/ingress.rb b/spec/factories/clusters/applications/ingress.rb deleted file mode 100644 index 85f54a9d744..00000000000 --- a/spec/factories/clusters/applications/ingress.rb +++ /dev/null @@ -1,35 +0,0 @@ -FactoryBot.define do - factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do - cluster factory: %i(cluster provided_by_gcp) - - trait :not_installable do - status(-2) - end - - trait :installable do - status 0 - end - - trait :scheduled do - status 1 - end - - trait :installing do - status 2 - end - - trait :installed do - status 3 - end - - trait :errored do - status(-1) - status_reason 'something went wrong' - end - - trait :timeouted do - installing - updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago - end - end -end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index 84a8bc56640..d5d819d862a 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -23,7 +23,7 @@ FactoryBot.define do end after(:build) do |commit, evaluator| - allow(commit).to receive(:author).and_return(evaluator.author || build(:author)) + allow(commit).to receive(:author).and_return(evaluator.author || build_stubbed(:author)) end trait :without_author do diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb index 30a6d468ed3..4350652fb79 100644 --- a/spec/factories/deploy_keys_projects.rb +++ b/spec/factories/deploy_keys_projects.rb @@ -2,5 +2,9 @@ FactoryBot.define do factory :deploy_keys_project do deploy_key project + + trait :write_access do + can_push true + end end end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 9d7d5e56611..cac56695319 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -3,13 +3,14 @@ FactoryBot.define do sha '97de212e80737a608d939f648d959671fb0a0142' ref 'master' tag false - user + user nil project nil deployable factory: :ci_build environment factory: :environment after(:build) do |deployment, evaluator| deployment.project ||= deployment.environment.project + deployment.user ||= deployment.project.creator unless deployment.project.repository_exists? allow(deployment.project.repository).to receive(:create_ref) diff --git a/spec/factories/events.rb b/spec/factories/events.rb index ed275243ac9..5798b81ecad 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :event do project - author factory: :user + author(factory: :user) { project.creator } action Event::JOINED trait(:created) { action Event::CREATED } diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 1512f5a0e58..8c531cf5909 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -18,7 +18,7 @@ FactoryBot.define do end trait :with_avatar do - avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } + avatar { fixture_file_upload('spec/fixtures/dk.png') } end trait :access_requestable do diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 5ed6b017dee..998080a3dd5 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :issue do title { generate(:title) } - author project + author { project.creator } trait :confidential do confidential true @@ -14,6 +14,7 @@ FactoryBot.define do trait :closed do state :closed + closed_at { Time.now } end factory :closed_issue, traits: [:closed] diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index e6eb76f71d3..23a98a899f1 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -5,6 +5,10 @@ FactoryBot.define do title key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' } + factory :key_without_comment do + key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate } + end + factory :deploy_key, class: 'DeployKey' factory :personal_key do @@ -15,18 +19,16 @@ FactoryBot.define do factory :another_deploy_key, class: 'DeployKey' end - factory :write_access_key, class: 'DeployKey' do - can_push true - end - factory :rsa_key_2048 do key do - 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \ - '6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \ - '/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \ - 'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \ - 'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \ - '5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 + 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 + /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 + M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC + rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 + 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com + KEY end factory :rsa_deploy_key_2048, class: 'DeployKey' @@ -34,37 +36,44 @@ FactoryBot.define do factory :dsa_key_2048 do key do - 'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \ - 'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \ - 'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \ - '/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \ - 'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \ - '5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \ - 'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \ - 'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \ - 'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \ - 'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \ - 'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \ - '1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \ - 'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \ - 'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \ - 'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \ - 'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \ - 'Taja+Cf9kMo== dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G + Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp + YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ + /pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz + OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv + 5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB + AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t + poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1 + M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH + MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H + nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A + 1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb + aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI + zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex + PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z + wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS + Taja+Cf9kMo== dummy@gitlab.com + KEY end end factory :ecdsa_key_256 do key do - 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \ - 'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \ - 'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com' + <<~KEY.delete("\n") + ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA + AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO + Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com + KEY end end factory :ed25519_key_256 do key do - 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJ + OhDRvf59ohL6 dummy@gitlab.com + KEY end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 40558c88d15..d26cb0c3417 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -1,9 +1,9 @@ FactoryBot.define do factory :merge_request do title { generate(:title) } - author association :source_project, :repository, factory: :project target_project { source_project } + author { source_project.creator } # $ git log --pretty=oneline feature..master # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 707ecbd6be5..3f4e408b3a6 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -6,7 +6,7 @@ FactoryBot.define do factory :note do project note { generate(:title) } - author + author { project&.creator || create(:user) } on_issue factory :note_on_commit, traits: [:on_commit] @@ -122,11 +122,11 @@ FactoryBot.define do end trait :with_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + attachment { fixture_file_upload(Rails.root.join( "spec/fixtures/dk.png"), "image/png") } end trait :with_svg_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") } + attachment { fixture_file_upload(Rails.root.join("spec/fixtures/unsanitized.svg"), "image/svg+xml") } end transient do diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb index 89d8248f9f4..db2eb4fc863 100644 --- a/spec/factories/project_wikis.rb +++ b/spec/factories/project_wikis.rb @@ -3,7 +3,7 @@ FactoryBot.define do skip_create project - user factory: :user + user { project.creator } initialize_with { new(project, user) } end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index d0f3911f730..20976977f21 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -90,7 +90,13 @@ FactoryBot.define do end trait :with_avatar do - avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } + avatar { fixture_file_upload('spec/fixtures/dk.png') } + end + + trait :with_export do + after(:create) do |project, evaluator| + ProjectExportWorker.new.perform(project.creator.id, project.id) + end end trait :broken_storage do diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 39460834d06..60956511834 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -53,6 +53,7 @@ FactoryBot.define do if evaluator.default_access_level && evaluator.default_push_level protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER) end + if evaluator.default_access_level && evaluator.default_merge_level protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER) end diff --git a/spec/factories/redirect_routes.rb b/spec/factories/redirect_routes.rb new file mode 100644 index 00000000000..c29c81c5df9 --- /dev/null +++ b/spec/factories/redirect_routes.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :redirect_route do + sequence(:path) { |n| "redirect#{n}" } + source factory: :group + permanent false + + trait :permanent do + permanent true + end + + trait :temporary do + permanent false + end + end +end diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb index 80872067233..b0174dd06b7 100644 --- a/spec/factories/sent_notifications.rb +++ b/spec/factories/sent_notifications.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :sent_notification do project - recipient factory: :user + recipient { project.creator } noteable { create(:issue, project: project) } reply_key { SentNotification.reply_key } end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 4b0377967c7..110ef33c6f7 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -18,6 +18,7 @@ FactoryBot.define do factory :kubernetes_service do project + type 'KubernetesService' active true properties({ api_url: 'https://kubernetes.example.com', diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 2ab9a56d255..dc12b562108 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -21,6 +21,7 @@ FactoryBot.define do factory :project_snippet, parent: :snippet, class: :ProjectSnippet do project + author { project.creator } end factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb index a4bc4e87b0a..8f7ab74ec70 100644 --- a/spec/factories/subscriptions.rb +++ b/spec/factories/subscriptions.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :subscription do - user project + user { project.creator } subscribable factory: :issue end end diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index af34b0681e2..b45f06b9a0a 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :timelog do time_spent 3600 - user issue + user { issue.project.creator } end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 6a6de665dd1..94f8caedfa6 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :todo do project - author - user + author { project.creator } + user { project.creator } target factory: :issue action { Todo::ASSIGNED } diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index c39500faea1..ff3a2a76acc 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -1,24 +1,46 @@ FactoryBot.define do factory :upload do model { build(:project) } - path { "uploads/-/system/project/avatar/avatar.jpg" } size 100.kilobytes uploader "AvatarUploader" + mount_point :avatar + secret nil - trait :personal_snippet do - model { build(:personal_snippet) } + # we should build a mount agnostic upload by default + transient do + filename 'myfile.jpg' + end + + # this needs to comply with RecordsUpload::Concern#upload_path + path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') } + + trait :personal_snippet_upload do uploader "PersonalFileUploader" + path { File.join(secret, filename) } + model { build(:personal_snippet) } + secret SecureRandom.hex end trait :issuable_upload do - path { "#{SecureRandom.hex}/myfile.jpg" } uploader "FileUploader" + path { File.join(secret, filename) } + secret SecureRandom.hex end trait :namespace_upload do - path { "#{SecureRandom.hex}/myfile.jpg" } model { build(:group) } + path { File.join(secret, filename) } uploader "NamespaceFileUploader" + secret SecureRandom.hex + end + + trait :attachment_upload do + transient do + mount_point :attachment + end + + model { build(:note) } + uploader "AttachmentUploader" end end end diff --git a/spec/factories/user_callouts.rb b/spec/factories/user_callouts.rb new file mode 100644 index 00000000000..528e442c14b --- /dev/null +++ b/spec/factories/user_callouts.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user_callout do + feature_name :gke_cluster_integration + + user + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index e62e0b263ca..769fd656e7a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -38,7 +38,7 @@ FactoryBot.define do end trait :with_avatar do - avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } + avatar { fixture_file_upload('spec/fixtures/dk.png') } end trait :two_factor_via_otp do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index e020579f71e..51b42d1b43b 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -21,7 +21,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page.all('.build-link').size).to eq(4) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -31,7 +31,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -51,7 +51,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) expect(page.find('.build-link')).not_to have_content(build4.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -63,7 +63,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -83,7 +83,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) expect(page.find('.build-link')).not_to have_content(build4.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -95,7 +95,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -113,7 +113,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).to have_content(build3.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -125,7 +125,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end end diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index 241c7cbc34e..cb96830cb7c 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -17,6 +17,16 @@ RSpec.describe 'admin deploy keys' do end end + it 'shows all the projects the deploy key has write access' do + write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key) + + visit admin_deploy_keys_path + + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content(write_key.project.full_name) + end + end + describe 'create a new deploy key' do let(:new_ssh_key) { attributes_for(:key)[:key] } @@ -28,14 +38,12 @@ RSpec.describe 'admin deploy keys' do it 'creates a new deploy key' do fill_in 'deploy_key_title', with: 'laptop' fill_in 'deploy_key_key', with: new_ssh_key - check 'deploy_key_can_push' click_button 'Create' expect(current_path).to eq admin_deploy_keys_path page.within(find('.deploy-keys-list', match: :first)) do expect(page).to have_content('laptop') - expect(page).to have_content('Yes') end end end @@ -48,14 +56,12 @@ RSpec.describe 'admin deploy keys' do it 'updates an existing deploy key' do fill_in 'deploy_key_title', with: 'new-title' - check 'deploy_key_can_push' click_button 'Save changes' expect(current_path).to eq admin_deploy_keys_path page.within(find('.deploy-keys-list', match: :first)) do expect(page).to have_content('new-title') - expect(page).to have_content('Yes') end end end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index ac3392b49f9..3693e5882f9 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -17,7 +17,7 @@ feature "Admin Health Check", :feature do page.has_text? 'Health Check' page.has_text? 'Health information can be retrieved' - token = current_application_settings.health_check_access_token + token = Gitlab::CurrentSettings.health_check_access_token expect(page).to have_content("Access token is #{token}") expect(page).to have_selector('#health-check-token', text: token) @@ -25,7 +25,7 @@ feature "Admin Health Check", :feature do describe 'reload access token' do it 'changes the access token' do - orig_token = current_application_settings.health_check_access_token + orig_token = Gitlab::CurrentSettings.health_check_access_token click_button 'Reset health check access token' expect(page).to have_content('New health check access token has been generated!') diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index eec44549a03..f266f2ecc54 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -describe 'Admin::Hooks', :js do - before do - @project = create(:project) - sign_in(create(:admin)) +describe 'Admin::Hooks' do + let(:user) { create(:admin) } - @system_hook = create(:system_hook) + before do + sign_in(user) end describe 'GET /admin/hooks' do @@ -13,15 +12,17 @@ describe 'Admin::Hooks', :js do visit admin_root_path page.within '.nav-sidebar' do - click_on 'Hooks' + click_on 'System Hooks', match: :first end expect(current_path).to eq(admin_hooks_path) end it 'has hooks list' do + system_hook = create(:system_hook) + visit admin_hooks_path - expect(page).to have_content(@system_hook.url) + expect(page).to have_content(system_hook.url) end end @@ -43,6 +44,10 @@ describe 'Admin::Hooks', :js do describe 'Update existing hook' do let(:new_url) { generate(:url) } + before do + create(:system_hook) + end + it 'updates existing hook' do visit admin_hooks_path @@ -57,7 +62,11 @@ describe 'Admin::Hooks', :js do end end - describe 'Remove existing hook' do + describe 'Remove existing hook', :js do + before do + create(:system_hook) + end + context 'removes existing hook' do it 'from hooks list page' do visit admin_hooks_path @@ -76,7 +85,8 @@ describe 'Admin::Hooks', :js do describe 'Test', :js do before do - WebMock.stub_request(:post, @system_hook.url) + system_hook = create(:system_hook) + WebMock.stub_request(:post, system_hook.url) visit admin_hooks_path find('.hook-test-button.dropdown').click @@ -85,4 +95,41 @@ describe 'Admin::Hooks', :js do it { expect(current_path).to eq(admin_hooks_path) } end + + context 'Merge request hook' do + describe 'New Hook' do + let(:url) { generate(:url) } + + it 'adds new hook' do + visit admin_hooks_path + + fill_in 'hook_url', with: url + uncheck 'Repository update events' + check 'Merge request events' + + expect { click_button 'Add system hook' }.to change(SystemHook, :count).by(1) + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(url) + end + end + + describe 'Test', :js do + before do + system_hook = create(:system_hook) + WebMock.stub_request(:post, system_hook.url) + end + + it 'succeeds if the user has a repository with a merge request' do + project = create(:project, :repository) + create(:project_member, user: user, project: project) + create(:merge_request, source_project: project) + + visit admin_hooks_path + find('.hook-test-button.dropdown').click + click_link 'Merge requests events' + + expect(page).to have_content 'Hook executed successfully' + end + end + end end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 94b12105088..d02ac6c2e2a 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -88,7 +88,7 @@ describe "Admin::Projects" do describe 'add admin himself to a project' do before do - project.team << [user, :master] + project.add_master(user) end it 'adds admin a to a project as developer', :js do @@ -110,8 +110,8 @@ describe "Admin::Projects" do describe 'admin remove himself from a project' do before do - project.team << [user, :master] - project.team << [current_user, :developer] + project.add_master(user) + project.add_developer(current_user) end it 'removes admin from the project' do diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index c1c54177167..a01c129defd 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -156,7 +156,7 @@ describe "Admin Runners" do end describe 'runners registration token' do - let!(:token) { current_application_settings.runners_registration_token } + let!(:token) { Gitlab::CurrentSettings.runners_registration_token } before do visit admin_runners_path diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 1218ea52227..cc0849d1cc6 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -38,12 +38,12 @@ feature 'Admin updates settings' do uncheck 'Project export enabled' click_button 'Save' - expect(current_application_settings.gravatar_enabled).to be_falsey - expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" - expect(current_application_settings.help_page_text).to eq "Example text" - expect(current_application_settings.help_page_hide_commercial_content).to be_truthy - expect(current_application_settings.help_page_support_url).to eq "http://example.com/help" - expect(current_application_settings.project_export_enabled).to be_falsey + expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey + expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/" + expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text" + expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy + expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help" + expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey expect(page).to have_content "Application settings saved successfully" end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 89c9d377003..d673bac4995 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -8,8 +8,8 @@ describe "Dashboard Issues Feed" do let!(:project2) { create(:project) } before do - project1.team << [user, :master] - project2.team << [user, :master] + project1.add_master(user) + project2.add_master(user) end describe "atom feed" do diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 2c0c331b6db..c6683bb3bc9 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -26,7 +26,7 @@ describe "Dashboard Feed" do let(:note) { create(:note, noteable: issue, author: user, note: 'Bug confirmed', project: project) } before do - project.team << [user, :master] + project.add_master(user) issue_event(issue, user) note_event(note, user) visit dashboard_projects_path(:atom, rss_token: user.rss_token) diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 4102ac0588a..525ce23aa56 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -9,7 +9,7 @@ describe 'Issues Feed' do let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) } before do - project.team << [user, :developer] + project.add_developer(user) group.add_developer(user) end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 2b934d81674..2d074c115dd 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -47,7 +47,7 @@ describe "User Feed" do let!(:push_event_payload) { create(:push_event_payload, event: push_event) } before do - project.team << [user, :master] + project.add_master(user) issue_event(issue, user) note_event(note, user) merge_request_event(merge_request, user) @@ -64,7 +64,7 @@ describe "User Feed" do end it 'has XHTML summaries in issue descriptions' do - expect(body).to match /<hr ?\/>/ + expect(body).to match %r{<hr ?/>} end it 'has XHTML summaries in notes' do @@ -72,7 +72,7 @@ describe "User Feed" do end it 'has XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ + expect(body).to match %r{Here is the fix: <a[^>]*><img[^>]*/></a>} end it 'has push event commit ID' do diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index 7a395f62511..9aef68b7156 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -52,7 +52,7 @@ describe 'Auto deploy' do context 'when user configured kubernetes from Integration > Kubernetes' do before do create :kubernetes_service, project: project - project.team << [user, :master] + project.add_master(user) sign_in user end @@ -65,7 +65,7 @@ describe 'Auto deploy' do context 'when user configured kubernetes from CI/CD > Clusters' do before do create(:cluster, :provided_by_gcp, projects: [project]) - project.team << [user, :master] + project.add_master(user) sign_in user end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index e4cfcea45a5..18901a954cc 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -12,7 +12,7 @@ describe 'Issue Boards add issue modal', :js do let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e8d779f5772..3d13f806b11 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -11,8 +11,8 @@ describe 'Issue Boards', :js do let!(:user2) { create(:user) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) set_cookie('sidebar_collapsed', 'true') @@ -69,6 +69,7 @@ describe 'Issue Boards', :js do let!(:backlog) { create(:label, project: project, name: 'Backlog') } let!(:closed) { create(:label, project: project, name: 'Closed') } let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } + let!(:a_plus) { create(:label, project: project, name: 'A+') } let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: development, position: 1) } @@ -83,6 +84,7 @@ describe 'Issue Boards', :js do let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } + let!(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } before do visit project_board_path(project, board) @@ -400,6 +402,15 @@ describe 'Issue Boards', :js do wait_for_empty_boards((3..4)) end + it 'filters by label with encoded character' do + set_filter("label", a_plus.title) + click_filter_link(a_plus.title) + submit_filter + + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..4)) + end + it 'filters by label with space after reload' do set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) @@ -551,7 +562,7 @@ describe 'Issue Boards', :js do let(:user_guest) { create(:user) } before do - project.team << [user_guest, :guest] + project.add_guest(user_guest) sign_out(:user) sign_in(user_guest) visit project_board_path(project, board) diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 4cbb48e2e6e..5abd02dbb48 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -13,7 +13,7 @@ describe 'Issue Boards', :js do let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb index 435de3861cf..d820a59aa16 100644 --- a/spec/features/boards/keyboard_shortcut_spec.rb +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -1,20 +1,38 @@ require 'rails_helper' describe 'Issue Boards shortcut', :js do - let(:project) { create(:project) } + context 'issues are enabled' do + let(:project) { create(:project) } - before do - create(:board, project: project) + before do + create(:board, project: project) - sign_in(create(:admin)) + sign_in(create(:admin)) - visit project_path(project) + visit project_path(project) + end + + it 'takes user to issue board index' do + find('body').native.send_keys('gb') + expect(page).to have_selector('.boards-list') + + wait_for_requests + end end - it 'takes user to issue board index' do - find('body').native.send_keys('gb') - expect(page).to have_selector('.boards-list') + context 'issues are not enabled' do + let(:project) { create(:project, :issues_disabled) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'does not take user to the issue board index' do + find('body').native.send_keys('gb') - wait_for_requests + expect(page).to have_selector("body[data-page='projects:show']") + end end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 422d96175f7..5907bb0840f 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -10,7 +10,7 @@ describe 'Issue Boards add issue modal filtering', :js do let!(:issue1) { create(:issue, project: project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -76,7 +76,7 @@ describe 'Issue Boards add issue modal filtering', :js do let!(:issue) { create(:issue, project: project, author: user2) } before do - project.team << [user2, :developer] + project.add_developer(user2) visit_board end @@ -99,7 +99,7 @@ describe 'Issue Boards add issue modal filtering', :js do let!(:issue) { create(:issue, project: project, assignees: [user2]) } before do - project.team << [user2, :developer] + project.add_developer(user2) visit_board end diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 5ac4d87e90b..6769acb7c9c 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -8,7 +8,7 @@ describe 'Issue Boards new issue', :js do context 'authorized user' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 205900615c4..b2dbfcd0031 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -334,14 +334,14 @@ describe 'Issue Boards', :js do wait_for_requests page.within('.subscriptions') do - click_button 'Subscribe' + find('.js-issuable-subscribe-button button:not(.is-checked)').click wait_for_requests - expect(page).to have_content('Unsubscribe') + expect(page).to have_css('.js-issuable-subscribe-button button.is-checked') end end - it 'has "Unsubscribe" button when already subscribed' do + it 'has checked subscription toggle when already subscribed' do create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true) visit project_board_path(project, board) wait_for_requests @@ -350,10 +350,10 @@ describe 'Issue Boards', :js do wait_for_requests page.within('.subscriptions') do - click_button 'Unsubscribe' + find('.js-issuable-subscribe-button button.is-checked').click wait_for_requests - expect(page).to have_content('Subscribe') + expect(page).to have_css('.js-issuable-subscribe-button button:not(.is-checked)') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 77dcdf89f37..62a2ec55b00 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -26,7 +26,7 @@ describe 'Commits' do let!(:status) { create(:generic_commit_status, pipeline: pipeline) } before do - project.team << [user, :reporter] + project.add_reporter(user) end describe 'Commit builds' do @@ -51,7 +51,7 @@ describe 'Commits' do context 'when logged as developer' do before do - project.team << [user, :developer] + project.add_developer(user) end describe 'Project commits' do @@ -145,7 +145,7 @@ describe 'Commits' do context "when logged as reporter" do before do - project.team << [user, :reporter] + project.add_reporter(user) build.update_attributes(legacy_artifacts_file: artifacts_file) visit pipeline_path(pipeline) end @@ -188,13 +188,13 @@ describe 'Commits' do let(:branch_name) { 'master' } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_commits_path(project, branch_name) end it 'includes the committed_date for each commit' do - commits = project.repository.commits(branch_name) + commits = project.repository.commits(branch_name, limit: 40) commits.each do |commit| expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}") diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 1fcb8d5bc67..f82ed6300cc 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -285,6 +285,102 @@ describe 'Copy as GFM', :js do end verify( + 'MermaidFilter: mermaid as converted from GFM to HTML', + + <<-GFM.strip_heredoc + ```mermaid + graph TD; + A-->B; + ``` + GFM + ) + + aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do + gfm = <<-GFM.strip_heredoc + ```mermaid + graph TD; + A-->B; + ``` + GFM + + html = <<-HTML.strip_heredoc + <svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid"> + <style> + .mermaid { + /* Flowchart variables */ + /* Sequence Diagram variables */ + /* Gantt chart variables */ + /** Section styling */ + /* Grid and axis */ + /* Today line */ + /* Task styling */ + /* Default task */ + /* Specific task settings for the sections*/ + /* Active task */ + /* Completed task */ + /* Tasks on the critical line */ + } + </style> + <g> + <g class="output"> + <g class="clusters"></g> + <g class="edgePaths"> + <g class="edgePath" style="opacity: 1;"> + <path class="path" d="M33.6171875,52L33.6171875,77L33.6171875,102" marker-end="url(#arrowhead65)" style="fill:none"></path> + <defs> + <marker id="arrowhead65" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"> + <path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path> + </marker> + </defs> + </g> + </g> + <g class="edgeLabels"> + <g class="edgeLabel" style="opacity: 1;" transform=""> + <g transform="translate(0,0)" class="label"> + <foreignObject width="0" height="0"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"> + <span class="edgeLabel"></span> + </div> + </foreignObject> + </g> + </g> + </g> + <g class="nodes"> + <g class="node" id="A" transform="translate(33.6171875,36)" style="opacity: 1;"> + <rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32"></rect> + <g class="label" transform="translate(0,0)"> + <g transform="translate(-3.6171875,-6)"> + <foreignObject width="7.234375" height="12"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">A</div> + </foreignObject> + </g> + </g> + </g> + <g class="node" id="B" transform="translate(33.6171875,118)" style="opacity: 1;"> + <rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32"> + </rect> + <g class="label" transform="translate(0,0)"> + <g transform="translate(-3.6171875,-6)"> + <foreignObject width="7.234375" height="12"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">B</div> + </foreignObject> + </g> + </g> + </g> + </g> + </g> + </g> + <text class="source" display="none">graph TD; + A-->B; + </text> + </svg> + HTML + + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + + verify( 'SanitizationFilter', <<-GFM.strip_heredoc @@ -654,7 +750,7 @@ describe 'Copy as GFM', :js do js = <<-JS.strip_heredoc (function(selector) { var els = document.querySelectorAll(selector); - var htmls = _.map(els, function(el) { return el.outerHTML; }); + var htmls = [].slice.call(els).map(function(el) { return el.outerHTML; }); return htmls.join("\\n"); })("#{escape_javascript(selector)}") JS diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 177cd50dd72..510677ecf56 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -95,7 +95,7 @@ feature 'Cycle Analytics', :js do before do user.update_attribute(:preferred_language, 'es') - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_cycle_analytics_path(project) wait_for_requests @@ -113,6 +113,7 @@ feature 'Cycle Analytics', :js do context "as a guest" do before do + project.add_developer(user) project.add_guest(guest) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index bd115785646..a74a8aac2b2 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -24,6 +24,7 @@ feature 'Dashboard > Activity' do end let(:note) { create(:note, project: project, noteable: merge_request) } + let(:milestone) { create(:milestone, :active, project: project, title: '1.0') } let!(:push_event) do event = create(:push_event, project: project, author: user) @@ -54,6 +55,10 @@ feature 'Dashboard > Activity' do create(:event, :commented, project: project, target: note, author: user) end + let!(:milestone_event) do + create(:event, :closed, project: project, target: milestone, author: user) + end + before do project.add_master(user) @@ -68,6 +73,7 @@ feature 'Dashboard > Activity' do expect(page).to have_content('accepted') expect(page).to have_content('closed') expect(page).to have_content('commented on') + expect(page).to have_content('closed milestone') end end @@ -107,6 +113,7 @@ feature 'Dashboard > Activity' do expect(page).not_to have_content('accepted') expect(page).to have_content('closed') expect(page).not_to have_content('commented on') + expect(page).to have_content('closed milestone') end end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index e8d699ff5e0..b36231fd78b 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'Dashboard Archived Project' do let(:archived_project) { create(:project, :archived) } before do - project.team << [user, :master] - archived_project.team << [user, :master] + project.add_master(user) + archived_project.add_master(user) sign_in(user) diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 349f9a47112..089c388636d 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -8,7 +8,7 @@ feature 'Tooltips on .timeago dates', :js do context 'on the activity tab' do before do - project.team << [user, :master] + project.add_master(user) Event.create( project: project, author_id: user.id, action: Event::JOINED, updated_at: created_date, created_at: created_date) @@ -27,7 +27,7 @@ feature 'Tooltips on .timeago dates', :js do context 'on the snippets tab' do before do - project.team << [user, :master] + project.add_master(user) create(:snippet, author: user, updated_at: created_date, created_at: created_date) sign_in user diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index d92c002b4e7..a71020002dc 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -94,22 +94,14 @@ feature 'Dashboard Groups page', :js do end it 'can toggle parent group' do - # Collapsed by default - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).to have_selector("#group-#{group.id} .fa-caret-right") - # expand click_group_caret(group) - expect(page).to have_selector("#group-#{group.id} .fa-caret-down") - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1) expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") # collapse click_group_caret(group) - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).to have_selector("#group-#{group.id} .fa-caret-right") expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") end end diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 5b4c00b3c7e..54652e2d849 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Dashboard Issues' do let!(:other_issue) { create :issue, project: project } before do - [project, project_with_issues_disabled].each { |project| project.team << [current_user, :master] } + [project, project_with_issues_disabled].each { |project| project.add_master(current_user) } sign_in(current_user) visit issues_dashboard_path(assignee_id: current_user.id) end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 991d360ccaf..744041ac425 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do context 'merge requests exist' do let!(:assigned_merge_request) do - create(:merge_request, assignee: current_user, target_project: project, source_project: project) + create(:merge_request, + assignee: current_user, + source_project: project, + author: create(:user)) end let!(:assigned_merge_request_from_fork) do create(:merge_request, source_branch: 'markdown', assignee: current_user, - target_project: public_project, source_project: forked_project - ) + target_project: public_project, source_project: forked_project, + author: create(:user)) end let!(:authored_merge_request) do create(:merge_request, - source_branch: 'markdown', author: current_user, - target_project: project, source_project: project - ) + source_branch: 'markdown', + source_project: project, + author: current_user) end let!(:authored_merge_request_from_fork) do create(:merge_request, source_branch: 'feature_conflict', author: current_user, - target_project: public_project, source_project: forked_project - ) + target_project: public_project, source_project: forked_project) end let!(:other_merge_request) do create(:merge_request, source_branch: 'fix', - target_project: project, source_project: project - ) + source_project: project, + author: create(:user)) end before do diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index 41d37376cfb..7787772a958 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -16,7 +16,7 @@ feature 'Dashboard > Milestones' do let(:project) { create(:project, namespace: user.namespace) } let!(:milestone) { create(:milestone, project: project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit dashboard_milestones_path end diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index 8f96899fb4f..6c3093607b0 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -5,7 +5,7 @@ feature 'Project member activity', :js do let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) end def visit_activities_and_wait_with_event(event_type) diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index fbf8b5c0db6..586c7b48d0b 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -6,7 +6,7 @@ feature 'Dashboard Projects' do let(:project2) { create(:project, :public, name: 'Community project') } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb index ad0f132da8c..2fc34301d51 100644 --- a/spec/features/dashboard/todos/todos_filtering_spec.rb +++ b/spec/features/dashboard/todos/todos_filtering_spec.rb @@ -15,8 +15,8 @@ feature 'Dashboard > User filters todos', :js do create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1) create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2) - project_1.team << [user_1, :developer] - project_2.team << [user_1, :developer] + project_1.add_developer(user_1) + project_2.add_developer(user_1) sign_in(user_1) visit dashboard_todos_path end @@ -66,8 +66,8 @@ feature 'Dashboard > User filters todos', :js do create(:todo, user: user_1, author: user_3, project: project_1, target: issue, action: 1, state: :done) create(:todo, user: user_1, author: user_4, project: project_2, target: merge_request, action: 2, state: :done) - project_1.team << [user_3, :developer] - project_2.team << [user_4, :developer] + project_1.add_developer(user_3) + project_2.add_developer(user_4) visit dashboard_todos_path(state: 'done') diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb index b7d39a872b0..10e3ad843fd 100644 --- a/spec/features/dashboard/todos/todos_sorting_spec.rb +++ b/spec/features/dashboard/todos/todos_sorting_spec.rb @@ -9,7 +9,7 @@ feature 'Dashboard > User sorts todos' do let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'sort options' do diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index c352b6ded14..92f4d4b854c 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -7,14 +7,14 @@ describe 'Dashboard > User filters projects' do let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end describe 'filtering personal projects' do before do - project2.team << [user, :developer] + project2.add_developer(user) visit dashboard_projects_path end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 1dd7547a7fc..31862b2e8f4 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do wait_for_requests end - it 'makes a request to get the content' do - ajax_uris = evaluate_script('ajaxUris') - - expect(ajax_uris).not_to be_empty - expect(ajax_uris.first).to include('large_diff.md') - end - it 'shows the diff content' do expect(large_diff).to have_selector('.code') expect(large_diff).not_to have_selector('.nothing-here-block') diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb new file mode 100644 index 00000000000..e4ef47d88dd --- /dev/null +++ b/spec/features/explore/groups_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe 'Explore Groups', :js do + let(:user) { create :user } + let(:group) { create :group } + let!(:private_project) do + create :project, :private, namespace: group do |project| + create(:issue, project: internal_project) + create(:merge_request, source_project: project, target_project: project) + end + end + + let!(:internal_project) do + create :project, :internal, namespace: group do |project| + create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) + end + end + + let!(:public_project) do + create(:project, :public, namespace: group) do |project| + create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) + end + end + + shared_examples 'renders public and internal projects' do + it do + visit_page + expect(page).to have_content(public_project.name) + expect(page).to have_content(internal_project.name) + expect(page).not_to have_content(private_project.name) + end + end + + shared_examples 'renders only public project' do + it do + visit_page + expect(page).to have_content(public_project.name) + expect(page).not_to have_content(internal_project.name) + expect(page).not_to have_content(private_project.name) + end + end + + shared_examples 'renders group in public groups area' do + it do + visit explore_groups_path + expect(page).to have_content(group.name) + end + end + + context 'when signed in' do + before do + sign_in(user) + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit group_path(group) } + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit issues_group_path(group) } + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit merge_requests_group_path(group) } + end + + it_behaves_like 'renders group in public groups area' + end + + context 'when signed out' do + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit group_path(group) } + end + + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit issues_group_path(group) } + end + + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit merge_requests_group_path(group) } + end + + it_behaves_like 'renders group in public groups area' + end +end diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index f04e13adba7..f8c4db1403c 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -5,7 +5,7 @@ feature 'Global search' do let(:project) { create(:project, namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -22,7 +22,7 @@ feature 'Global search' do click_button "Go" select_filter("Issues") - expect(page).to have_selector('.gl-pagination .page', count: 2) + expect(page).to have_selector('.gl-pagination .next') end end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index cdf7aceb13c..450bc0ff8cf 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -3,40 +3,61 @@ require 'spec_helper' feature 'Group issues page' do include FilteredSearchHelpers - let(:path) { issues_group_path(group) } - let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} + context 'with shared examples' do + let(:path) { issues_group_path(group) } + let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} - include_examples 'project features apply to issuables', Issue + include_examples 'project features apply to issuables', Issue - context 'rss feed' do - let(:access_level) { ProjectFeature::ENABLED } + context 'rss feed' do + let(:access_level) { ProjectFeature::ENABLED } - context 'when signed in' do - let(:user) { user_in_group } + context 'when signed in' do + let(:user) { user_in_group } - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" - end + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + end - context 'when signed out' do - let(:user) { nil } + context 'when signed out' do + let(:user) { nil } - it_behaves_like "it has an RSS button without an RSS token" - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" + end end - end - context 'assignee', :js do - let(:access_level) { ProjectFeature::ENABLED } - let(:user) { user_in_group } - let(:user2) { user_outside_group } - let(:path) { issues_group_path(group) } + context 'assignee', :js do + let(:access_level) { ProjectFeature::ENABLED } + let(:user) { user_in_group } + let(:user2) { user_outside_group } + let(:path) { issues_group_path(group) } + + it 'filters by only group users' do + filtered_search.set('assignee:') - it 'filters by only group users' do - filtered_search.set('assignee:') + expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) + expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) + end + end + end - expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) - expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) + context 'issues list', :nested_groups do + let(:group) { create(:group)} + let(:subgroup) { create(:group, parent: group) } + let(:project) { create(:project, :public, group: group)} + let(:subgroup_project) { create(:project, :public, group: subgroup)} + let!(:issue) { create(:issue, project: project, title: 'root group issue') } + let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') } + + it 'returns all group and subgroup issues' do + visit issues_group_path(group) + + page.within('.issuable-list') do + expect(page).to have_selector('li.issue', count: 2) + expect(page).to have_content('root group issue') + expect(page).to have_content('subgroup issue') + end end end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 7fc2b383749..ceccc471405 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -55,4 +55,20 @@ feature 'Group show page' do end end end + + context 'group has a project with emoji in description', :js do + let(:user) { create(:user) } + let!(:project) { create(:project, description: ':smile:', namespace: group) } + + before do + group.add_owner(user) + sign_in(user) + visit path + end + + it 'shows the project info' do + expect(page).to have_content(project.title) + expect(page).to have_selector('gl-emoji[data-name="smile"]') + end + end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb new file mode 100644 index 00000000000..e4be6193b8b --- /dev/null +++ b/spec/features/invites_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe 'Invites' do + let(:user) { create(:user) } + let(:owner) { create(:user, name: 'John Doe') } + let(:group) { create(:group, name: 'Owned') } + let(:project) { create(:project, :repository, namespace: group) } + let(:invite) { group.group_members.invite.last } + + before do + project.add_master(owner) + group.add_user(owner, Gitlab::Access::OWNER) + group.add_developer('user@example.com', owner) + invite.generate_invite_token! + end + + context 'when signed out' do + before do + visit invite_path(invite.raw_invite_token) + end + + it 'renders sign in page with sign in notice' do + expect(current_path).to eq(new_user_session_path) + expect(page).to have_content('To accept this invitation, sign in') + end + + it 'sign in and redirects to invitation page' do + fill_in 'user_login', with: user.email + fill_in 'user_password', with: user.password + check 'user_remember_me' + click_button 'Sign in' + + expect(current_path).to eq(invite_path(invite.raw_invite_token)) + expect(page).to have_content( + 'You have been invited by John Doe to join group Owned as Developer.' + ) + expect(page).to have_link('Accept invitation') + expect(page).to have_link('Decline') + end + end + + context 'when signed in as an exists member' do + before do + sign_in(owner) + end + + it 'shows message user already a member' do + visit invite_path(invite.raw_invite_token) + expect(page).to have_content('However, you are already a member of this group.') + end + end + + describe 'accepting the invitation' do + before do + sign_in(user) + visit invite_path(invite.raw_invite_token) + end + + it 'grants access and redirects to group page' do + page.click_link 'Accept invitation' + expect(current_path).to eq(group_path(group)) + expect(page).to have_content( + 'You have been granted Developer access to group Owned.' + ) + end + end + + describe 'declining the application' do + context 'when signed in' do + before do + sign_in(user) + visit invite_path(invite.raw_invite_token) + end + + it 'declines application and redirects to dashboard' do + page.click_link 'Decline' + expect(current_path).to eq(dashboard_projects_path) + expect(page).to have_content( + 'You have declined the invitation to join group Owned.' + ) + end + end + + context 'when signed out' do + before do + visit decline_invite_path(invite.raw_invite_token) + end + + it 'declines application and redirects to sign in page' do + expect(current_path).to eq(new_user_session_path) + expect(page).to have_content( + 'You have declined the invitation to join group Owned.' + ) + end + end + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 850b35c4467..1131e1711bf 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -11,7 +11,7 @@ describe 'Awards Emoji' do context 'authorized user' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index fa4d3a55c62..cf283119f36 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -11,7 +11,7 @@ feature 'Issues > Labels bulk assignment' do context 'as an allowed user', :js do before do - project.team << [user, :master] + project.add_master(user) sign_in user end @@ -377,6 +377,7 @@ feature 'Issues > Labels bulk assignment' do items.map do |item| click_link item end + if unmark items.map do |item| # Make sure we are unmarking the item no matter the state it has currently diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index 822ba48e005..e0466aaf422 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -8,7 +8,7 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d describe 'as a user with access to the project' do before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_merge_request_path(project, merge_request) end @@ -81,7 +81,7 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d describe 'as a reporter' do before do - project.team << [user, :reporter] + project.add_reporter(user) sign_in user visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) end diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb index f0bed85595c..34beb282bad 100644 --- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb @@ -8,7 +8,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do describe 'As a user with access to the project' do before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_merge_request_path(project, merge_request) end @@ -65,7 +65,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do describe 'as a reporter' do before do - project.team << [user, :reporter] + project.add_reporter(user) sign_in user visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 2e4a25ee15d..cbd0949c192 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -20,9 +20,9 @@ describe 'Dropdown assignee', :js do end before do - project.team << [user, :master] - project.team << [user_john, :master] - project.team << [user_jacob, :master] + project.add_master(user) + project.add_master(user_john) + project.add_master(user_jacob) sign_in(user) create(:issue, project: project) @@ -222,7 +222,7 @@ describe 'Dropdown assignee', :js do expect(initial_size).to be > 0 new_user = create(:user) - project.team << [new_user, :master] + project.add_master(new_user) find('.filtered-search-box .clear-search').click filtered_search.set('assignee') filtered_search.send_keys(':') diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 2fb5e7cdba4..70b4f11410d 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -28,9 +28,9 @@ describe 'Dropdown author', :js do end before do - project.team << [user, :master] - project.team << [user_john, :master] - project.team << [user_jacob, :master] + project.add_master(user) + project.add_master(user_john) + project.add_master(user_jacob) sign_in(user) create(:issue, project: project) @@ -195,7 +195,7 @@ describe 'Dropdown author', :js do expect(initial_size).to be > 0 new_user = create(:user) - project.team << [new_user, :master] + project.add_master(new_user) find('.filtered-search-box .clear-search').click filtered_search.set('author') send_keys_to_filtered_search(':') diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index 8db435634fd..436625a6f7b 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -28,7 +28,7 @@ describe 'Dropdown emoji', :js do end before do - project.team << [user, :master] + project.add_master(user) create_list(:award_emoji, 2, user: user, name: 'thumbsup') create_list(:award_emoji, 1, user: user, name: 'thumbsdown') create_list(:award_emoji, 3, user: user, name: 'star') diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 0183495a1db..ef40dddfd3a 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -13,7 +13,7 @@ describe 'Dropdown hint', :js do end before do - project.team << [user, :master] + project.add_master(user) create(:issue, project: project) end @@ -176,6 +176,7 @@ describe 'Dropdown hint', :js do it 'reuses existing author text' do filtered_search.send_keys('author:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('author') expect_tokens([{ name: 'author' }]) @@ -185,6 +186,7 @@ describe 'Dropdown hint', :js do it 'reuses existing assignee text' do filtered_search.send_keys('assignee:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('assignee') expect_tokens([{ name: 'assignee' }]) @@ -194,6 +196,7 @@ describe 'Dropdown hint', :js do it 'reuses existing milestone text' do filtered_search.send_keys('milestone:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('milestone') expect_tokens([{ name: 'milestone' }]) @@ -203,6 +206,7 @@ describe 'Dropdown hint', :js do it 'reuses existing label text' do filtered_search.send_keys('label:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('label') expect_tokens([{ name: 'label' }]) @@ -212,6 +216,7 @@ describe 'Dropdown hint', :js do it 'reuses existing emoji text' do filtered_search.send_keys('my-reaction:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('my-reaction') expect_tokens([{ name: 'my-reaction' }]) diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 031eb06723a..94710c2f71f 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -29,7 +29,7 @@ describe 'Dropdown milestone', :js do end before do - project.team << [user, :master] + project.add_master(user) sign_in(user) create(:issue, project: project) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 88688422dc7..268590da599 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -8,7 +8,7 @@ describe 'Search bar', :js do let(:filtered_search) { find('.filtered-search') } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) create(:issue, project: project) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 2db6f9a2982..faf14be4818 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -13,8 +13,8 @@ describe 'New/edit issue', :js do let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) sign_in(user) end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 6a9a80235c1..f2624f55c86 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -7,7 +7,7 @@ feature 'GFM autocomplete', :js do let(:issue) { create(:issue, project: project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_issue_path(project, issue) diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index a9de52bd8d5..64b4f9e7e67 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -8,6 +8,7 @@ feature 'Issue Sidebar' do let(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } + let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } before do sign_in(user) @@ -18,7 +19,7 @@ feature 'Issue Sidebar' do let(:issue2) { create(:issue, project: project, author: user2) } before do - project.team << [user, :developer] + project.add_developer(user) visit_issue(project, issue2) find('.block.assignee .edit-link').click @@ -78,7 +79,7 @@ feature 'Issue Sidebar' do context 'as a allowed user' do before do - project.team << [user, :developer] + project.add_developer(user) visit_issue(project, issue) end @@ -99,6 +100,14 @@ feature 'Issue Sidebar' do restore_window_size open_issue_sidebar end + + it 'escapes XSS when viewing issue labels' do + page.within('.block.labels') do + find('.edit-link').click + + expect(page).to have_content '<script>alert("xss");</script>' + end + end end context 'editing issue labels', :js do @@ -156,7 +165,7 @@ feature 'Issue Sidebar' do context 'as a guest' do before do - project.team << [user, :guest] + project.add_guest(user) visit_issue(project, issue) end diff --git a/spec/features/issues/keyboard_shortcut_spec.rb b/spec/features/issues/keyboard_shortcut_spec.rb new file mode 100644 index 00000000000..961de9d3d25 --- /dev/null +++ b/spec/features/issues/keyboard_shortcut_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe 'Issues shortcut', :js do + context 'New Issue shortcut' do + context 'issues are enabled' do + let(:project) { create(:project) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'takes user to the new issue page' do + find('body').native.send_keys('i') + expect(page).to have_selector('#new_issue') + end + end + + context 'issues are not enabled' do + let(:project) { create(:project, :issues_disabled) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'does not take user to the new issue page' do + find('body').native.send_keys('i') + + expect(page).to have_selector("body[data-page='projects:show']") + end + end + end +end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 17035b5501c..076a02150a4 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -13,7 +13,7 @@ feature 'issue move to another project' do context 'user does not have permission to move issue' do background do - old_project.team << [user, :guest] + old_project.add_guest(user) visit issue_path(issue) end @@ -31,8 +31,8 @@ feature 'issue move to another project' do let(:cross_reference) { old_project.to_reference(new_project) } background do - old_project.team << [user, :reporter] - new_project.team << [user, :reporter] + old_project.add_reporter(user) + new_project.add_reporter(user) visit issue_path(issue) end @@ -50,7 +50,7 @@ feature 'issue move to another project' do end scenario 'searching project dropdown', :js do - new_project_search.team << [user, :reporter] + new_project_search.add_reporter(user) find('.js-move-issue').click wait_for_requests @@ -66,7 +66,7 @@ feature 'issue move to another project' do context 'user does not have permission to move the issue to a project', :js do let!(:private_project) { create(:project, :private) } let(:another_project) { create(:project) } - background { another_project.team << [user, :guest] } + background { another_project.add_guest(user) } scenario 'browsing projects in projects select' do find('.js-move-issue').click diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb index 05c93a19253..f08c73f947c 100644 --- a/spec/features/issues/notes_on_issues_spec.rb +++ b/spec/features/issues/notes_on_issues_spec.rb @@ -8,7 +8,7 @@ describe 'Create notes on issues', :js do let(:note_text) { "Check #{mention.to_reference}" } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit project_issue_path(project, issue) diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index d25231d624c..a75ca1d42b3 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -9,7 +9,7 @@ describe 'New issue', :js do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - current_application_settings.update!( + Gitlab::CurrentSettings.update!( akismet_enabled: true, akismet_api_key: 'testkey', recaptcha_enabled: true, @@ -17,7 +17,7 @@ describe 'New issue', :js do recaptcha_private_key: 'test private key' ) - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -34,6 +34,9 @@ describe 'New issue', :js do click_button 'Submit issue' + # reCAPTCHA alerts when it can't contact the server, so just accept it and move on + page.driver.browser.switch_to.alert.accept + # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha # recaptcha verification is skipped in test environment and it always returns true expect(page).not_to have_content('issue title') diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 29a2d38ae18..8e6493bbd93 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -6,7 +6,7 @@ feature 'Manually create a todo item from issue', :js do let!(:user) { create(:user)} before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_issue_path(project, issue) end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index bcc6e9bab0f..7d6edc171f8 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -6,7 +6,7 @@ feature 'Multiple issue updating from issues#index', :js do let!(:user) { create(:user)} before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index c4c06ed514b..e711a191db2 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -12,7 +12,7 @@ feature 'Issues > User uses quick actions', :js do let(:project) { create(:project, :public) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_issue_path(project, issue) end @@ -50,7 +50,7 @@ feature 'Issues > User uses quick actions', :js do context 'when the current user cannot update the due date' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) gitlab_sign_out sign_in(guest) visit project_issue_path(project, issue) @@ -90,7 +90,7 @@ feature 'Issues > User uses quick actions', :js do context 'when the current user cannot update the due date' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) gitlab_sign_out sign_in(guest) visit project_issue_path(project, issue) @@ -138,7 +138,7 @@ feature 'Issues > User uses quick actions', :js do context 'when the current user cannot update the issue' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) gitlab_sign_out sign_in(guest) visit project_issue_path(project, issue) @@ -163,7 +163,7 @@ feature 'Issues > User uses quick actions', :js do let(:target_project) { create(:project, :public) } before do - target_project.team << [user, :master] + target_project.add_master(user) sign_in(user) visit project_issue_path(project, issue) end @@ -220,7 +220,7 @@ feature 'Issues > User uses quick actions', :js do let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') } before do - target_project.team << [user, :master] + target_project.add_master(user) sign_in(user) visit project_issue_path(project, issue) end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d1ff057a0c6..314bd19f586 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -25,7 +25,8 @@ describe 'Issues' do sign_in(user) user2 = create(:user) - project.team << [[user, user2], :developer] + project.add_developer(user) + project.add_developer(user2) end describe 'empty state' do @@ -383,7 +384,7 @@ describe 'Issues' do before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") - project1.team << [user, :master] + project1.add_master(user) visit namespace_project_issues_path(user.namespace, project1) end @@ -491,7 +492,7 @@ describe 'Issues' do let(:guest) { create(:user) } before do - project.team << [[guest], :guest] + project.add_guest(guest) end it 'shows assignee text', :js do @@ -552,7 +553,7 @@ describe 'Issues' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) issue.milestone = milestone issue.save end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index e285befc66f..f13d78d24e3 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -71,7 +71,7 @@ describe 'GitLab Markdown' do it 'parses mermaid code block' do aggregate_failures do - expect(doc).to have_selector('pre.code.js-render-mermaid') + expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid') end end @@ -259,6 +259,10 @@ describe 'GitLab Markdown' do it 'includes VideoLinkFilter' do expect(doc).to parse_video_links end + + it 'includes ColorFilter' do + expect(doc).to parse_colors + end end context 'wiki pipeline' do @@ -320,6 +324,10 @@ describe 'GitLab Markdown' do it 'includes VideoLinkFilter' do expect(doc).to parse_video_links end + + it 'includes ColorFilter' do + expect(doc).to parse_colors + end end # Fake a `current_user` helper diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb new file mode 100644 index 00000000000..b6b38186a22 --- /dev/null +++ b/spec/features/merge_request/user_assigns_themselves_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +describe 'Merge request > User assigns themselves' do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:issue1) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue1.to_reference} and #{issue2.to_reference}") } + + context 'logged in as a member of the project' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'updates related issues', :js do + click_link 'Assign yourself to these issues' + + expect(page).to have_content '2 issues have been assigned to you' + end + + it 'returns user to the merge request', :js do + click_link 'Assign yourself to these issues' + + expect(page).to have_content merge_request.description + end + + context 'when related issues are already assigned' do + before do + [issue1, issue2].each { |issue| issue.update!(assignees: [user]) } + end + + it 'does not display if related issues are already assigned' do + expect(page).not_to have_content 'Assign yourself' + end + end + end + + context 'logged in as a non-member of the project' do + before do + sign_in(create(:user)) + visit project_merge_request_path(project, merge_request) + end + + it 'does not not show assignment link' do + expect(page).not_to have_content 'Assign yourself' + end + end +end diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb index a24464f2556..2f24cfbd9e3 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_request/user_awards_emoji_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' -feature 'Merge request awards', :js do - let(:user) { create(:user) } +describe 'Merge request > User awards emoji', :js do let(:project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, source_project: project) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) } describe 'logged in' do before do diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_request/user_cherry_picks_spec.rb index 48f370c3ad4..494096b21c0 100644 --- a/spec/features/merge_requests/cherry_pick_spec.rb +++ b/spec/features/merge_request/user_cherry_picks_spec.rb @@ -1,17 +1,17 @@ -require 'spec_helper' +require 'rails_helper' -describe 'Cherry-pick Merge Requests', :js do - let(:user) { create(:user) } +describe 'Merge request > User cherry-picks', :js do let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: group) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) } before do - sign_in user - project.team << [user, :master] + project.add_master(user) + sign_in(user) end - context "Viewing a merged merge request" do + context 'Viewing a merged merge request' do before do service = MergeRequests::MergeService.new(project, user) @@ -21,24 +21,24 @@ describe 'Cherry-pick Merge Requests', :js do end # Fast-forward merge, or merged before GitLab 8.5. - context "Without a merge commit" do + context 'Without a merge commit' do before do merge_request.merge_commit_sha = nil merge_request.save end - it "doesn't show a Cherry-pick button" do + it 'does not show a Cherry-pick button' do visit project_merge_request_path(project, merge_request) - expect(page).not_to have_link "Cherry-pick" + expect(page).not_to have_link 'Cherry-pick' end end - context "With a merge commit" do - it "shows a Cherry-pick button" do + context 'With a merge commit' do + it 'shows a Cherry-pick button' do visit project_merge_request_path(project, merge_request) - expect(page).to have_link "Cherry-pick" + expect(page).to have_link 'Cherry-pick' end end end diff --git a/spec/features/merge_requests/image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index b53570835cb..7c4fd25bb39 100644 --- a/spec/features/merge_requests/image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -1,14 +1,13 @@ require 'spec_helper' -feature 'image diff notes', :js do +feature 'Merge request > User creates image diff notes', :js do include NoteInteractionHelpers - let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } before do - project.team << [user, :master] - sign_in user + sign_in(user) # Stub helper to return any blob file as image from public app folder. # This is necessary to run this specs since we don't display repo images in capybara. @@ -25,20 +24,14 @@ feature 'image diff notes', :js do create_image_diff_note end - it 'shows indicator badge on image diff' do + it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do indicator = find('.js-image-badge') - - expect(indicator).to have_content('1') - end - - it 'shows the avatar badge on the new note' do badge = find('.image-diff-avatar-link .badge') + expect(indicator).to have_content('1') expect(badge).to have_content('1') - end - it 'allows collapsing/expanding the discussion notes' do - find('.js-diff-notes-toggle', :first).click + find('.js-diff-notes-toggle').click expect(page).not_to have_content('image diff test comment') @@ -86,15 +79,9 @@ feature 'image diff notes', :js do wait_for_requests end - it 'render diff indicators within the image diff frame' do + it 'render diff indicators within the image diff frame, diff notes, and avatar badge numbers' do expect(page).to have_css('.js-image-badge', count: 2) - end - - it 'shows the diff notes' do expect(page).to have_css('.diff-content .note', count: 2) - end - - it 'shows the diff notes with correct avatar badge numbers' do expect(page).to have_css('.image-diff-avatar-link', text: 1) expect(page).to have_css('.image-diff-avatar-link', text: 2) end @@ -127,19 +114,13 @@ feature 'image diff notes', :js do create_image_diff_note end - it 'shows indicator badge on image diff' do + it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do indicator = find('.js-image-badge', match: :first) - - expect(indicator).to have_content('1') - end - - it 'shows the avatar badge on the new note' do badge = find('.image-diff-avatar-link .badge', match: :first) + expect(indicator).to have_content('1') expect(badge).to have_content('1') - end - it 'allows expanding/collapsing the discussion notes' do page.all('.js-diff-notes-toggle')[0].click page.all('.js-diff-notes-toggle')[1].click @@ -154,7 +135,7 @@ feature 'image diff notes', :js do end end - describe 'discussion tab polling', :js do + describe 'discussion tab polling' do let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) } let(:path) { "files/images/ee_repo_logo.png" } diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb new file mode 100644 index 00000000000..1ac31de62cb --- /dev/null +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe 'Merge request > User creates MR' do + it_behaves_like 'a creatable merge request' + + context 'from a forked project' do + include ProjectForksHelper + + let(:canonical_project) { create(:project, :public, :repository) } + + let(:source_project) do + fork_project(canonical_project, user, + repository: true, + namespace: user.namespace) + end + + context 'to canonical project' do + it_behaves_like 'a creatable merge request' + end + + context 'to another forked project' do + let(:target_project) do + fork_project(canonical_project, user, + repository: true, + namespace: user.namespace) + end + + it_behaves_like 'a creatable merge request' + end + end +end diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb index 82b2b56ef80..e1e70b6d260 100644 --- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb +++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb @@ -1,8 +1,8 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Clicking toggle commit message link', :js do - let(:user) { create(:user) } +describe 'Merge request < User customizes merge commit message', :js do let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } let(:issue_1) { create(:issue, project: project)} let(:issue_2) { create(:issue, project: project)} let(:merge_request) do @@ -32,18 +32,15 @@ feature 'Clicking toggle commit message link', :js do end before do - project.team << [user, :master] - - sign_in user - + project.add_master(user) + sign_in(user) visit project_merge_request_path(project, merge_request) + end + it 'toggles commit message between message with description and without description' do expect(page).not_to have_selector('.js-commit-message') click_button "Modify commit message" expect(textbox).to be_visible - end - - it "toggles commit message between message with description and without description " do expect(textbox.value).to eq(default_message) click_link "Include description in commit message" diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb new file mode 100644 index 00000000000..8c9e782aa76 --- /dev/null +++ b/spec/features/merge_request/user_edits_mr_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +describe 'Merge request > User edits MR' do + it_behaves_like 'an editable merge request' + + context 'for a forked project' do + it_behaves_like 'an editable merge request' do + let(:source_project) { create(:project, :repository, forked_from_project: target_project) } + end + end +end diff --git a/spec/features/merge_requests/discussion_lock_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb index 7bbd3b1e69e..a68df872334 100644 --- a/spec/features/merge_requests/discussion_lock_spec.rb +++ b/spec/features/merge_request/user_locks_discussion_spec.rb @@ -1,9 +1,9 @@ -require 'spec_helper' +require 'rails_helper' -describe 'Discussion Lock', :js do +describe 'Merge request > User locks discussion', :js do let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project, author: user) } let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, source_project: project) } before do sign_in(user) diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index 0b5a595acce..b16fc9bfc89 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -1,9 +1,8 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge immediately', :js do - let(:user) { create(:user) } +describe 'Merge requests > User merges immediately', :js do let(:project) { create(:project, :public, :repository) } - + let(:user) { project.creator } let!(:merge_request) do create(:merge_request_with_diffs, source_project: project, author: user, @@ -11,25 +10,18 @@ feature 'Merge immediately', :js do head_pipeline: pipeline, source_branch: pipeline.ref) end - let(:pipeline) do create(:ci_pipeline, project: project, ref: 'master', sha: project.repository.commit('master').id) end - before do - project.team << [user, :master] - end - context 'when there is active pipeline for merge request' do - background do - create(:ci_build, pipeline: pipeline) - end - before do - sign_in user - visit project_merge_request_path(merge_request.project, merge_request) + create(:ci_build, pipeline: pipeline) + project.add_master(user) + sign_in(user) + visit project_merge_request_path(project, merge_request) end it 'enables merge immediately' do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb index 91f207bd339..a045791f6b4 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb @@ -1,18 +1,17 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Only allow merge requests to be merged if the pipeline succeeds', :js do +describe 'Merge request > User merges only if pipeline succeeds', :js do let(:merge_request) { create(:merge_request_with_diffs) } let(:project) { merge_request.target_project } before do - sign_in merge_request.author - - project.team << [merge_request.author, :master] + project.add_master(merge_request.author) + sign_in(merge_request.author) end - context 'project does not have CI enabled', :js do + context 'project does not have CI enabled' do it 'allows MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -20,8 +19,8 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end end - context 'when project has CI enabled', :js do - given!(:pipeline) do + context 'when project has CI enabled' do + let!(:pipeline) do create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, @@ -35,10 +34,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI is running' do - given(:status) { :running } + let(:status) { :running } it 'does not allow to merge immediately' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -48,10 +47,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI failed' do - given(:status) { :failed } + let(:status) { :failed } it 'does not allow MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -61,10 +60,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI canceled' do - given(:status) { :canceled } + let(:status) { :canceled } it 'does not allow MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -74,10 +73,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI succeeded' do - given(:status) { :success } + let(:status) { :success } it 'allows MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -86,10 +85,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI skipped' do - given(:status) { :skipped } + let(:status) { :skipped } it 'allows MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -104,10 +103,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI is running' do - given(:status) { :running } + let(:status) { :running } it 'allows MR to be merged immediately' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -119,10 +118,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI failed' do - given(:status) { :failed } + let(:status) { :failed } it 'allows MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -131,10 +130,10 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end context 'when CI succeeded' do - given(:status) { :success } + let(:status) { :success } it 'allows MR to be merged' do - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) wait_for_requests @@ -143,8 +142,4 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d end end end - - def visit_merge_request(merge_request) - visit project_merge_request_path(merge_request.project, merge_request) - end end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index ac46cc1f0e4..890774922aa 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -1,16 +1,14 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge When Pipeline Succeeds', :js do - let(:user) { create(:user) } +describe 'Merge request > User merges when pipeline succeeds', :js do let(:project) { create(:project, :public, :repository) } - + let(:user) { project.creator } let(:merge_request) do create(:merge_request_with_diffs, source_project: project, author: user, title: 'Bug NS-04', merge_params: { force_remove_source_branch: '1' }) end - let(:pipeline) do create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, @@ -23,17 +21,10 @@ feature 'Merge When Pipeline Succeeds', :js do end context 'when there is active pipeline for merge request' do - background do - create(:ci_build, pipeline: pipeline) - end - before do - sign_in user - visit_merge_request(merge_request) - end - - it 'displays the Merge when pipeline succeeds button' do - expect(page).to have_button "Merge when pipeline succeeds" + create(:ci_build, pipeline: pipeline) + sign_in(user) + visit project_merge_request_path(project, merge_request) end describe 'enabling Merge when pipeline succeeds' do @@ -44,7 +35,7 @@ feature 'Merge When Pipeline Succeeds', :js do expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" expect(page).to have_content "The source branch will not be removed" expect(page).to have_selector ".js-cancel-auto-merge" - visit_merge_request(merge_request) # Needed to refresh the page + visit project_merge_request_path(project, merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i end end @@ -115,14 +106,13 @@ feature 'Merge When Pipeline Succeeds', :js do title: 'MepMep', merge_when_pipeline_succeeds: true) end - let!(:build) do create(:ci_build, pipeline: pipeline) end before do sign_in user - visit_merge_request(merge_request) + visit project_merge_request_path(project, merge_request) end it 'allows to cancel the automatic merge' do @@ -130,31 +120,67 @@ feature 'Merge When Pipeline Succeeds', :js do expect(page).to have_button "Merge when pipeline succeeds" - visit_merge_request(merge_request) # refresh the page + refresh + expect(page).to have_content "canceled the automatic merge" end context 'when pipeline succeeds' do - background { build.success } + before do + build.success + refresh + end it 'merges merge request' do - visit_merge_request(merge_request) # refresh the page - expect(page).to have_content 'The changes were merged' expect(merge_request.reload).to be_merged end end + + context 'view merge request with MWPS enabled but automatically merge fails' do + before do + merge_request.update( + merge_user: merge_request.author, + merge_error: 'Something went wrong' + ) + refresh + end + + it 'shows information about the merge error' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_requests + + page.within('.mr-widget-body') do + expect(page).to have_content('Something went wrong') + end + end + end + + context 'view merge request with MWPS enabled but automatically merge fails' do + before do + merge_request.update( + merge_user: merge_request.author, + merge_error: 'Something went wrong' + ) + refresh + end + + it 'shows information about the merge error' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_requests + + page.within('.mr-widget-body') do + expect(page).to have_content('Something went wrong') + end + end + end end context 'when pipeline is not active' do - it "does not allow to enable merge when pipeline succeeds" do - visit_merge_request(merge_request) + it 'does not allow to enable merge when pipeline succeeds' do + visit project_merge_request_path(project, merge_request) expect(page).not_to have_link 'Merge when pipeline succeeds' end end - - def visit_merge_request(merge_request) - visit project_merge_request_path(merge_request.project, merge_request) - end end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index d44eb23d7f4..2b4623d6dc9 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -1,11 +1,15 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge requests > User posts diff notes', :js do +describe 'Merge request > User posts diff notes', :js do include MergeRequestDiffHelpers - let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } + let(:user) { project.creator } + let(:comment_button_class) { '.add-diff-note' } + let(:notes_holder_input_class) { 'js-temp-notes-holder' } + let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } + let(:test_note_comment) { 'this is a test note!' } before do set_cookie('sidebar_collapsed', 'true') @@ -14,11 +18,6 @@ feature 'Merge requests > User posts diff notes', :js do sign_in(user) end - let(:comment_button_class) { '.add-diff-note' } - let(:notes_holder_input_class) { 'js-temp-notes-holder' } - let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } - let(:test_note_comment) { 'this is a test note!' } - context 'when hovering over a parallel view diff file' do before do visit diffs_project_merge_request_path(project, merge_request, view: 'parallel') diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index e17e9c2ccf5..50d06565fc0 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +require 'rails_helper' -describe 'Merge requests > User posts notes', :js do +describe 'Merge request > User posts notes', :js do include NoteInteractionHelpers let(:project) { create(:project, :repository) } + let(:user) { project.creator } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) end @@ -13,7 +14,8 @@ describe 'Merge requests > User posts notes', :js do end before do - sign_in(create(:admin)) + project.add_master(user) + sign_in(user) visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 4e2963c116d..19995559fae 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -1,8 +1,8 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge request conflict resolution', :js do - let(:user) { create(:user) } +describe 'Merge request > User resolves conflicts', :js do let(:project) { create(:project, :repository) } + let(:user) { project.creator } before do # In order to have the diffs collapsed, we need to disable the increase feature @@ -88,7 +88,7 @@ feature 'Merge request conflict resolution', :js do context 'can be resolved in the UI' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end @@ -100,12 +100,12 @@ feature 'Merge request conflict resolution', :js do end it 'shows a link to the conflict resolution page' do - expect(page).to have_link('conflicts', href: /\/conflicts\Z/) + expect(page).to have_link('conflicts', href: %r{/conflicts\Z}) end context 'in Inline view mode' do before do - click_link('conflicts', href: /\/conflicts\Z/) + click_link('conflicts', href: %r{/conflicts\Z}) end include_examples "conflicts are resolved in Interactive mode" @@ -114,7 +114,7 @@ feature 'Merge request conflict resolution', :js do context 'in Parallel view mode' do before do - click_link('conflicts', href: /\/conflicts\Z/) + click_link('conflicts', href: %r{/conflicts\Z}) click_button 'Side-by-side' end @@ -128,7 +128,7 @@ feature 'Merge request conflict resolution', :js do before do visit project_merge_request_path(project, merge_request) - click_link('conflicts', href: /\/conflicts\Z/) + click_link('conflicts', href: %r{/conflicts\Z}) end it 'conflicts can not be resolved in Interactive mode' do @@ -175,14 +175,13 @@ feature 'Merge request conflict resolution', :js do let(:merge_request) { create_merge_request(source_branch) } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) - visit project_merge_request_path(project, merge_request) end it 'does not show a link to the conflict resolution page' do - expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/) + expect(page).not_to have_link('conflicts', href: %r{/conflicts\Z}) end it 'shows an error if the conflicts page is visited directly' do diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 15d380b1bf4..3e83a549682 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -1,10 +1,11 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Diff notes resolve', :js do - let(:user) { create(:user) } +describe 'Merge request > User resolves diff notes and discussions', :js do let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:guest) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } - let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } + let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } let(:path) { "files/ruby/popen.rb" } let(:position) do Gitlab::Diff::Position.new( @@ -18,8 +19,8 @@ feature 'Diff notes resolve', :js do context 'no discussions' do before do - project.team << [user, :master] - sign_in user + project.add_master(user) + sign_in(user) note.destroy visit_merge_request end @@ -32,8 +33,8 @@ feature 'Diff notes resolve', :js do context 'as authorized user' do before do - project.team << [user, :master] - sign_in user + project.add_master(user) + sign_in(user) visit_merge_request end @@ -67,6 +68,8 @@ feature 'Diff notes resolve', :js do click_button 'Resolve discussion' end + expect(page).to have_selector('.discussion-body', visible: false) + page.within '.diff-content .note' do expect(page).to have_selector('.line-resolve-btn.is-active') end @@ -105,7 +108,7 @@ feature 'Diff notes resolve', :js do it 'shows resolved discussion when toggled' do find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click - expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible + expect(page.find(".timeline-content #note_#{note.id}")).to be_visible end end @@ -318,9 +321,7 @@ feature 'Diff notes resolve', :js do end it 'shows jump to next discussion button' do - page.all('.discussion-reply-holder').each do |holder| - expect(holder).to have_selector('.discussion-next-btn') - end + expect(page.all('.discussion-reply-holder')).to all(have_selector('.discussion-next-btn')) end it 'displays next discussion even if hidden' do @@ -426,11 +427,9 @@ feature 'Diff notes resolve', :js do end context 'as a guest' do - let(:guest) { create(:user) } - before do - project.team << [guest, :guest] - sign_in guest + project.add_guest(guest) + sign_in(guest) end context 'someone elses merge request' do @@ -456,10 +455,10 @@ feature 'Diff notes resolve', :js do end context 'guest users merge request' do + let(:user) { guest } + before do - mr = create(:merge_request_with_diffs, source_project: project, source_branch: 'markdown', author: guest, title: "Bug") - create(:diff_note_on_merge_request, project: project, noteable: mr) - visit_merge_request(mr) + visit_merge_request end it 'allows user to mark a note as resolved' do @@ -521,7 +520,7 @@ feature 'Diff notes resolve', :js do end def visit_merge_request(mr = nil) - mr = mr || merge_request + mr ||= merge_request visit project_merge_request_path(mr.project, mr) end end diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb index 25abbb469ab..9ba9e8b9585 100644 --- a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb +++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Resolve outdated diff discussions', :js do +feature 'Merge request > User resolves outdated diff discussions', :js do let(:project) { create(:project, :repository, :public) } let(:merge_request) do diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb new file mode 100644 index 00000000000..8a834adbf17 --- /dev/null +++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe 'Merge request > User scrolls to note on load', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, source_project: project, author: user) } + let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + let(:fragment_id) { "#note_#{note.id}" } + + before do + sign_in(user) + page.current_window.resize_to(1000, 300) + visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}" + end + + it 'scrolls down to fragment' do + page_height = page.current_window.size[1] + page_scroll_y = page.evaluate_script("window.scrollY") + fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)") + + expect(find('.js-toggle-content').visible?).to eq true + expect(find(fragment_id).visible?).to eq true + expect(fragment_position_top).to be >= page_scroll_y + expect(fragment_position_top).to be < (page_scroll_y + page_height) + end +end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index 9e816cf041b..9c0a04405a6 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -1,10 +1,10 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Diff note avatars', :js do +describe 'Merge request > User sees avatars on diff notes', :js do include NoteInteractionHelpers - let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } let(:path) { "files/ruby/popen.rb" } let(:position) do @@ -19,7 +19,7 @@ feature 'Diff note avatars', :js do let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) } before do - project.team << [user, :master] + project.add_master(user) sign_in user set_cookie('sidebar_collapsed', 'true') @@ -151,7 +151,6 @@ feature 'Diff note avatars', :js do page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') - find('.js-comment-button').click wait_for_requests @@ -169,7 +168,6 @@ feature 'Diff note avatars', :js do context 'multiple comments' do before do create_list(:diff_note_on_merge_request, 3, project: project, noteable: merge_request, in_reply_to: note) - visit diffs_project_merge_request_path(project, merge_request, view: view) wait_for_requests diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb index 4dd4e40f52c..726f35557a7 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb @@ -1,8 +1,8 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge Request closing issues message', :js do - let(:user) { create(:user) } +describe 'Merge request > User sees closing issues message', :js do let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } let(:issue_1) { create(:issue, project: project)} let(:issue_2) { create(:issue, project: project)} let(:merge_request) do @@ -18,10 +18,8 @@ feature 'Merge Request closing issues message', :js do let(:merge_request_title) { 'Merge Request Title' } before do - project.team << [user, :master] - - sign_in user - + project.add_master(user) + sign_in(user) visit project_merge_request_path(project, merge_request) wait_for_requests end diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb new file mode 100644 index 00000000000..01115318370 --- /dev/null +++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe 'Merge request > User sees deleted target branch', :js do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:user) { project.creator } + + before do + project.add_master(user) + DeleteBranchService.new(project, user).execute('feature') + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'shows a message about missing target branch' do + expect(page).to have_content('Target branch does not exist') + end + + it 'does not show link to target branch' do + expect(page).not_to have_selector('.mr-widget-body .js-branch-text a') + end +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 new file mode 100644 index 00000000000..3abe363d523 --- /dev/null +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -0,0 +1,56 @@ +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(:environment) { create(:environment, project: project) } + let(:role) { :developer } + let(:sha) { project.commit('master').id } + let!(:deployment) { create(:deployment, environment: environment, sha: sha) } + let!(:manual) { } + + before do + project.add_user(user, role) + sign_in(user) + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'displays that the environment is deployed' do + wait_for_requests + + expect(page).to have_content("Deployed to #{environment.name}") + expect(find('.js-deploy-time')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) + 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 + wait_for_requests + end + + it 'does start build when stop button clicked' do + accept_confirm { click_button('Stop environment') } + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + let(:role) { :reporter } + + it 'does not show stop button' do + expect(page).not_to have_button('Stop environment') + end + end + end + end +end diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb index 1bf77296ae6..a9063f2bcb3 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_request/user_sees_diff_spec.rb @@ -1,6 +1,6 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Diffs URL', :js do +describe 'Merge request > User sees diff', :js do include ProjectForksHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index 05789bbd31d..d6e8c8e86ba 100644 --- a/spec/features/merge_requests/discussion_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -1,19 +1,20 @@ -require 'spec_helper' +require 'rails_helper' + +describe 'Merge request > User sees discussions' do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, source_project: project) } -feature 'Merge Request Discussions' do before do - sign_in(create(:admin)) + project.add_master(user) + sign_in(user) end describe "Diff discussions" do - let(:merge_request) { create(:merge_request, importing: true) } - let(:project) { merge_request.source_project } let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) } let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create } - let!(:outdated_discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position).to_discussion } let!(:active_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion } - let(:outdated_position) do Gitlab::Diff::Position.new( old_path: "files/ruby/popen.rb", @@ -23,7 +24,6 @@ feature 'Merge Request Discussions' do diff_refs: outdated_diff_refs ) end - let(:outdated_diff_refs) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs } before do @@ -50,9 +50,6 @@ feature 'Merge Request Discussions' do end describe 'Commit comments displayed in MR context', :js do - let(:merge_request) { create(:merge_request) } - let(:project) { merge_request.project } - shared_examples 'a functional discussion' do let(:discussion_id) { note.discussion_id(merge_request) } diff --git a/spec/features/merge_request/user_sees_empty_state_spec.rb b/spec/features/merge_request/user_sees_empty_state_spec.rb new file mode 100644 index 00000000000..a939c7e9001 --- /dev/null +++ b/spec/features/merge_request/user_sees_empty_state_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Merge request > User sees empty state' do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + + before do + project.add_master(user) + sign_in(user) + end + + it 'shows an empty state and a "New merge request" button' do + visit project_merge_requests_path(project) + + expect(page).to have_selector('.empty-state') + expect(page).to have_link 'New merge request', href: project_new_merge_request_path(project) + end + + context 'if there are merge requests' do + before do + create(:merge_request, source_project: project) + + visit project_merge_requests_path(project) + end + + it 'does not show an empty state' do + expect(page).not_to have_selector('.empty-state') + end + end +end diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb index fbbfe7942be..85df43df38e 100644 --- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb @@ -1,24 +1,23 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Check if mergeable with unresolved discussions', :js do - let(:user) { create(:user) } +describe 'Merge request > User sees merge button depending on unresolved discussions', :js do let(:project) { create(:project, :repository) } + let(:user) { project.creator } let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) } before do - sign_in user - project.team << [user, :master] + project.add_master(user) + sign_in(user) end context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do before do project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true) + visit project_merge_request_path(project, merge_request) end context 'with unresolved discussions' do it 'does not allow to merge' do - visit_merge_request(merge_request) - expect(page).not_to have_button 'Merge' expect(page).to have_content('There are unresolved discussions.') end @@ -27,11 +26,10 @@ feature 'Check if mergeable with unresolved discussions', :js do context 'with all discussions resolved' do before do merge_request.discussions.each { |d| d.resolve!(user) } + visit project_merge_request_path(project, merge_request) end it 'allows MR to be merged' do - visit_merge_request(merge_request) - expect(page).to have_button 'Merge' end end @@ -40,12 +38,11 @@ feature 'Check if mergeable with unresolved discussions', :js do context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do before do project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false) + visit project_merge_request_path(project, merge_request) end context 'with unresolved discussions' do it 'does not allow to merge' do - visit_merge_request(merge_request) - expect(page).to have_button 'Merge' end end @@ -53,17 +50,12 @@ feature 'Check if mergeable with unresolved discussions', :js do context 'with all discussions resolved' do before do merge_request.discussions.each { |d| d.resolve!(user) } + visit project_merge_request_path(project, merge_request) end it 'allows MR to be merged' do - visit_merge_request(merge_request) - expect(page).to have_button 'Merge' end end end - - def visit_merge_request(merge_request) - visit project_merge_request_path(merge_request.project, merge_request) - end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 3ee094c216e..56224e505d9 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' -describe 'Merge request', :js do - let(:user) { create(:user) } +describe 'Merge request > User sees merge widget', :js do let(:project) { create(:project, :repository) } let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) } @@ -273,7 +273,7 @@ describe 'Merge request', :js do let(:user2) { create(:user) } before do - project.team << [user2, :master] + project.add_master(user2) sign_out(:user) sign_in(user2) merge_request.update(target_project: fork_project) diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index a7e7c0eeff6..a43ba05c64c 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -1,16 +1,14 @@ require 'rails_helper' -feature 'Mini Pipeline Graph', :js do - let(:user) { create(:user) } +describe 'Merge request < User sees mini pipeline graph', :js do let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project, head_pipeline: pipeline) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } before do build.run - sign_in(user) visit_merge_request end @@ -19,13 +17,13 @@ feature 'Mini Pipeline Graph', :js do visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) end - it 'should display a mini pipeline graph' do + it 'displays a mini pipeline graph' do expect(page).to have_selector('.mr-widget-pipeline-graph') end context 'as json' do - let(:artifacts_file1) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:artifacts_file2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') } + let(:artifacts_file1) { fixture_file_upload(Rails.root.join('spec/fixtures/banana_sample.gif'), 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } before do create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file1) @@ -51,7 +49,7 @@ feature 'Mini Pipeline Graph', :js do first('.mini-pipeline-graph-dropdown-toggle') end - it 'should expand when hovered' do + it 'expands when hovered' do find('.mini-pipeline-graph-dropdown-toggle') before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") @@ -63,13 +61,13 @@ feature 'Mini Pipeline Graph', :js do expect(before_width).to be < after_width end - it 'should show dropdown caret when hovered' do + it 'shows dropdown caret when hovered' do toggle.hover expect(toggle).to have_selector('.fa-caret-down') end - it 'should show tooltip when hovered' do + it 'shows tooltip when hovered' do toggle.hover expect(page).to have_selector('.tooltip') @@ -87,17 +85,17 @@ feature 'Mini Pipeline Graph', :js do wait_for_requests end - it 'should open when toggle is clicked' do + it 'pens when toggle is clicked' do expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'should close when toggle is clicked again' do + it 'closes when toggle is clicked again' do toggle.click expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'should close when clicking somewhere else' do + it 'closes when clicking somewhere else' do find('body').click expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') @@ -109,14 +107,14 @@ feature 'Mini Pipeline Graph', :js do first('.mini-pipeline-graph-dropdown-item') end - it 'should visit the build page when clicked' do + it 'visits the build page when clicked' do build_item.click find('.build-page') expect(current_path).to eql(project_job_path(project, build)) end - it 'should show tooltip when hovered' do + it 'shows tooltip when hovered' do build_item.hover expect(page).to have_selector('.tooltip') diff --git a/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb new file mode 100644 index 00000000000..029b66b5e8e --- /dev/null +++ b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe 'Merge request > User sees MR from deleted forked project', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:fork_project) { create(:project, :public, :repository, forked_from_project: project) } + let!(:merge_request) do + create(:merge_request_with_diffs, source_project: fork_project, + target_project: project, + description: 'Test merge request') + end + + before do + MergeRequests::MergeService.new(project, user).execute(merge_request) + fork_project.destroy! + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'user can access merge request' do + expect(page).to have_content 'Test merge request' + expect(page).to have_content "(removed):#{merge_request.source_branch}" + end +end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb index 7f69e82af4c..c1608be402a 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb @@ -1,23 +1,21 @@ -require 'spec_helper' +require 'rails_helper' # This test serves as a regression test for a bug that caused an error # message to be shown by JavaScript when the source branch was deleted. -# Please do not remove "js: true". -describe 'Deleted source branch', :js do - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request) } +# Please do not remove ":js". +describe 'Merge request > User sees MR with deleted source branch', :js do + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:user) { project.creator } before do - sign_in user - merge_request.project.team << [user, :master] merge_request.update!(source_branch: 'this-branch-does-not-exist') - visit project_merge_request_path(merge_request.project, merge_request) + sign_in(user) + visit project_merge_request_path(project, merge_request) end it 'shows a message about missing source branch' do - expect(page).to have_content( - 'Source branch does not exist.' - ) + expect(page).to have_content('Source branch does not exist.') end it 'still contains Discussion, Commits and Changes tabs' do diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb new file mode 100644 index 00000000000..b4cda269852 --- /dev/null +++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe 'Merge request > User sees notes from forked project', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:fork_project) { create(:project, :public, :repository, forked_from_project: project) } + let!(:merge_request) do + create(:merge_request_with_diffs, source_project: fork_project, + target_project: project, + description: 'Test merge request') + end + + before do + create(:note_on_commit, note: 'A commit comment', + project: fork_project, + commit_id: merge_request.commit_shas.first) + sign_in(user) + end + + it 'user can reply to the comment' do + visit project_merge_request_path(project, merge_request) + + expect(page).to have_content('A commit comment') + + page.within('.discussion-notes') do + find('.btn-text-field').click + find('#note_note').send_keys('A reply comment') + find('.comment-btn').click + end + + wait_for_requests + + expect(page).to have_content('A reply comment') + end +end diff --git a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb new file mode 100644 index 00000000000..d30dcefc6aa --- /dev/null +++ b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +describe 'Merge request > User sees pipelines from forked project', :js do + let(:target_project) { create(:project, :public, :repository) } + let(:user) { target_project.creator } + let(:fork_project) { create(:project, :repository, forked_from_project: target_project) } + let!(:merge_request) do + create(:merge_request_with_diffs, source_project: fork_project, + target_project: target_project, + description: 'Test merge request') + end + let(:pipeline) do + create(:ci_pipeline, + project: fork_project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch) + end + + before do + create(:ci_build, pipeline: pipeline, name: 'rspec') + create(:ci_build, pipeline: pipeline, name: 'spinach') + + sign_in(user) + visit project_merge_request_path(target_project, merge_request) + end + + it 'user visits a pipelines page' do + page.within('.merge-request-tabs') { click_link 'Pipelines' } + + page.within('.ci-table') do + expect(page).to have_content(pipeline.id) + end + end +end diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index 307c860eac4..a42c016392b 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -1,14 +1,14 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Pipelines for Merge Requests', :js do +describe 'Merge request > User sees pipelines', :js do describe 'pipeline tab' do - given(:user) { create(:user) } - given(:merge_request) { create(:merge_request) } - given(:project) { merge_request.target_project } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.target_project } + let(:user) { project.creator } before do - project.team << [user, :master] - sign_in user + project.add_master(user) + sign_in(user) end context 'with pipelines' do @@ -23,7 +23,7 @@ feature 'Pipelines for Merge Requests', :js do merge_request.update_attribute(:head_pipeline_id, pipeline.id) end - scenario 'user visits merge request pipelines tab' do + it 'user visits merge request pipelines tab' do visit project_merge_request_path(project, merge_request) expect(page.find('.ci-widget')).to have_content('pending') @@ -36,7 +36,7 @@ feature 'Pipelines for Merge Requests', :js do expect(page).to have_selector('.stage-cell') end - scenario 'pipeline sha does not equal last commit sha' do + it 'pipeline sha does not equal last commit sha' do pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434') visit project_merge_request_path(project, merge_request) wait_for_requests @@ -51,7 +51,7 @@ feature 'Pipelines for Merge Requests', :js do visit project_merge_request_path(project, merge_request) end - scenario 'user visits merge request page' do + it 'user visits merge request page' do page.within('.merge-request-tabs') do expect(page).to have_no_link('Pipelines') end @@ -60,22 +60,22 @@ feature 'Pipelines for Merge Requests', :js do end describe 'race condition' do - given(:project) { create(:project, :repository) } - given(:user) { create(:user) } - given(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } } - given(:merge_request_params) do + let(:merge_request_params) do { "source_branch" => "feature", "source_project_id" => project.id, "target_branch" => "master", "target_project_id" => project.id, "title" => "A" } end - background do + before do project.add_master(user) sign_in user end context 'when pipeline and merge request were created simultaneously' do - background do + before do stub_ci_pipeline_to_return_yaml_file threads = [] @@ -91,7 +91,7 @@ feature 'Pipelines for Merge Requests', :js do threads.each { |thr| thr.join } end - scenario 'user sees pipeline in merge request widget' do + it 'user sees pipeline in merge request widget' do visit project_merge_request_path(project, @merge_request) expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature']) diff --git a/spec/features/merge_requests/user_sees_system_notes_spec.rb b/spec/features/merge_request/user_sees_system_notes_spec.rb index 03dc61c2efa..a00a682757d 100644 --- a/spec/features/merge_requests/user_sees_system_notes_spec.rb +++ b/spec/features/merge_request/user_sees_system_notes_spec.rb @@ -1,15 +1,15 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge requests > User sees system notes' do +describe 'Merge request > User sees system notes' do let(:public_project) { create(:project, :public, :repository) } let(:private_project) { create(:project, :private, :repository) } + let(:user) { private_project.creator } let(:issue) { create(:issue, project: private_project) } let(:merge_request) { create(:merge_request, source_project: public_project, source_branch: 'markdown') } let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: public_project, note: "mentioned in #{issue.to_reference(public_project)}") } context 'when logged-in as a member of the private project' do before do - user = create(:user) private_project.add_developer(user) sign_in(user) end diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 482f2e51c8b..3a15d70979a 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -1,15 +1,17 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Merge Request versions', :js do +describe 'Merge request > User sees versions', :js do let(:merge_request) { create(:merge_request, importing: true) } let(:project) { merge_request.source_project } + let(:user) { project.creator } let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) } let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } - let!(:params) { Hash.new } + let!(:params) { {} } before do - sign_in(create(:admin)) + project.add_master(user) + sign_in(user) visit diffs_project_merge_request_path(project, merge_request, params) end @@ -62,19 +64,10 @@ feature 'Merge Request versions', :js do end end - it 'should show older version' do - page.within '.mr-version-dropdown' do - expect(page).to have_content 'version 1' - end - + it 'shows comments that were last relevant at that version' do expect(page).to have_content '5 changed files' - end - - it 'show the message about comments' do expect(page).to have_content 'Not all comments are displayed' - end - it 'shows comments that were last relevant at that version' do position = Gitlab::Diff::Position.new( old_path: ".gitmodules", new_path: ".gitmodules", @@ -86,7 +79,7 @@ feature 'Merge Request versions', :js do outdated_diff_note.position = outdated_diff_note.original_position outdated_diff_note.save! - visit current_url + refresh expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']") end @@ -110,26 +103,16 @@ feature 'Merge Request versions', :js do end end - it 'has a path with comparison context' do + it 'has a path with comparison context and shows comments that were last relevant at that version' do expect(page).to have_current_path diffs_project_merge_request_path( project, merge_request.iid, diff_id: merge_request_diff3.id, start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' ) - end - - it 'should have correct value in the compare dropdown' do - page.within '.mr-version-compare-dropdown' do - expect(page).to have_content 'version 1' - end - end - - it 'show the message about comments' do + expect(page).to have_content '4 changed files with 15 additions and 6 deletions' expect(page).to have_content 'Not all comments are displayed' - end - it 'shows comments that were last relevant at that version' do position = Gitlab::Diff::Position.new( old_path: ".gitmodules", new_path: ".gitmodules", @@ -141,7 +124,7 @@ feature 'Merge Request versions', :js do outdated_diff_note.position = outdated_diff_note.original_position outdated_diff_note.save! - visit current_url + refresh wait_for_requests expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']") @@ -151,7 +134,7 @@ feature 'Merge Request versions', :js do expect(page).to have_content '4 changed files with 15 additions and 6 deletions' end - it 'should return to latest version when "Show latest version" button is clicked' do + it 'returns to latest version when "Show latest version" button is clicked' do click_link 'Show latest version' page.within '.mr-version-dropdown' do expect(page).to have_content 'latest version' @@ -173,7 +156,7 @@ feature 'Merge Request versions', :js do end end - it 'should have 0 chages between versions' do + it 'has 0 chages between versions' do page.within '.mr-version-compare-dropdown' do expect(find('.dropdown-toggle')).to have_content 'version 1' end @@ -194,7 +177,7 @@ feature 'Merge Request versions', :js do end end - it 'should set the compared versions to be the same' do + it 'sets the compared versions to be the same' do page.within '.mr-version-compare-dropdown' do expect(find('.dropdown-toggle')).to have_content 'version 2' end diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb index b422c76249d..bc25243244e 100644 --- a/spec/features/merge_requests/wip_message_spec.rb +++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb @@ -1,11 +1,11 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Work In Progress help message' do - let!(:project) { create(:project, :public, :repository) } - let!(:user) { create(:user) } +describe 'Merge request > User sees WIP help message' do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index db5ce2d11a8..dbca279569a 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -1,13 +1,12 @@ -require 'spec_helper' +require 'rails_helper' -feature 'Create New Merge Request', :js do - let(:user) { create(:user) } +describe 'Merge request > User selects branches for new MR', :js do let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } before do - project.team << [user, :master] - - sign_in user + project.add_master(user) + sign_in(user) end it 'selects the source branch sha when a tag with the same name exists' do @@ -62,7 +61,7 @@ feature 'Create New Merge Request', :js do fill_in "merge_request_title", with: "Orphaned MR test" click_button "Submit merge request" - click_link "Check out branch" + click_button "Check out branch" expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' end diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb index fa3d988b27a..2e95a628013 100644 --- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb +++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb @@ -1,10 +1,13 @@ -require 'spec_helper' +require 'rails_helper' + +describe 'Merge request > User toggles whitespace changes', :js do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:user) { project.creator } -feature 'Toggle Whitespace Changes', :js do before do - sign_in(create(:admin)) - merge_request = create(:merge_request) - project = merge_request.source_project + project.add_master(user) + sign_in(user) visit diffs_project_merge_request_path(project, merge_request) end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_request/user_uses_slash_commands_spec.rb index ee0766f1192..bd739e69d6c 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_request/user_uses_slash_commands_spec.rb @@ -1,8 +1,14 @@ require 'rails_helper' -feature 'Merge Requests > User uses quick actions', :js do +describe 'Merge request > User uses quick actions', :js do include QuickActionsHelpers + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:guest) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } + let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } + it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do let(:issuable) { create(:merge_request, source_project: project) } let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } @@ -15,20 +21,12 @@ feature 'Merge Requests > User uses quick actions', :js do let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_merge_request_path(project, merge_request) end - after do - wait_for_requests - end - describe 'time tracking' do - before do - visit project_merge_request_path(project, merge_request) - end - it_behaves_like 'issuable time tracker' end @@ -56,9 +54,8 @@ feature 'Merge Requests > User uses quick actions', :js do end context 'when the current user cannot toggle the WIP prefix' do - let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) @@ -102,9 +99,8 @@ feature 'Merge Requests > User uses quick actions', :js do end context 'when the current user cannot merge the MR' do - let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) @@ -134,7 +130,7 @@ feature 'Merge Requests > User uses quick actions', :js do before do sign_out(:user) - another_project.team << [user, :master] + another_project.add_master(user) sign_in(user) end @@ -186,9 +182,8 @@ feature 'Merge Requests > User uses quick actions', :js do end context 'when current user can not change target branch' do - let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb deleted file mode 100644 index d49d145f254..00000000000 --- a/spec/features/merge_requests/assign_issues_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -feature 'Merge request issue assignment', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:issue1) { create(:issue, project: project) } - let(:issue2) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue1.to_reference} and #{issue2.to_reference}") } - let(:service) { MergeRequests::AssignIssuesService.new(merge_request, user, user, project) } - - before do - project.team << [user, :developer] - end - - def visit_merge_request(current_user = nil) - sign_in(current_user || user) - visit project_merge_request_path(project, merge_request) - end - - context 'logged in as author' do - it 'updates related issues' do - visit_merge_request - click_link "Assign yourself to these issues" - - expect(page).to have_content "2 issues have been assigned to you" - end - - it 'returns user to the merge request' do - visit_merge_request - click_link "Assign yourself to these issues" - - expect(page).to have_content merge_request.description - end - - it "doesn't display if related issues are already assigned" do - [issue1, issue2].each { |issue| issue.update!(assignees: [user]) } - - visit_merge_request - - expect(page).not_to have_content "Assign yourself" - end - end - - context 'not MR author' do - it "doesn't not show assignment link" do - visit_merge_request(create(:user)) - - expect(page).not_to have_content "Assign yourself" - end - end -end diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb deleted file mode 100644 index 93c40ff6443..00000000000 --- a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'spec_helper' - -feature 'Creating a merge request from a fork', :js do - include ProjectForksHelper - - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let!(:source_project) do - fork_project(project, user, - repository: true, - namespace: user.namespace) - end - - before do - source_project.add_master(user) - - sign_in(user) - end - - shared_examples 'create merge request to other project' do - it 'has all possible target projects' do - visit project_new_merge_request_path(source_project) - - first('.js-target-project').click - - within('.dropdown-target-project .dropdown-content') do - expect(page).to have_content(project.full_path) - expect(page).to have_content(target_project.full_path) - expect(page).to have_content(source_project.full_path) - end - end - - it 'allows creating the merge request to another target project' do - visit project_merge_requests_path(source_project) - - page.within '.content' do - click_link 'New merge request' - end - - find('.js-source-branch', match: :first).click - find('.dropdown-source-branch .dropdown-content a', match: :first).click - - first('.js-target-project').click - find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click - - click_button 'Compare branches and continue' - - wait_for_requests - - expect { click_button 'Submit merge request' } - .to change { target_project.merge_requests.reload.size }.by(1) - end - - it 'updates the branches when selecting a new target project' do - target_project_member = target_project.owner - CreateBranchService.new(target_project, target_project_member) - .execute('a-brand-new-branch-to-test', 'master') - visit project_new_merge_request_path(source_project) - - first('.js-target-project').click - find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click - - wait_for_requests - - first('.js-target-branch').click - - within('.dropdown-target-branch .dropdown-content') do - expect(page).to have_content('a-brand-new-branch-to-test') - end - end - end - - context 'creating to the source of a fork' do - let!(:target_project) { project } - - it_behaves_like('create merge request to other project') - end - - context 'creating to a sibling of a fork' do - let!(:target_project) do - other_user = create(:user) - fork_project(project, other_user, - repository: true, - namespace: other_user.namespace) - end - - it_behaves_like('create merge request to other project') - end -end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb deleted file mode 100644 index ca2225318cd..00000000000 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'spec_helper' - -feature 'Merge request created from fork' do - include ProjectForksHelper - - given(:user) { create(:user) } - given(:project) { create(:project, :public, :repository) } - given(:forked_project) { fork_project(project, user, repository: true) } - - given!(:merge_request) do - create(:merge_request_with_diffs, source_project: forked_project, - target_project: project, - description: 'Test merge request') - end - - background do - forked_project.team << [user, :master] - sign_in user - end - - scenario 'user can access merge request' do - visit_merge_request(merge_request) - - expect(page).to have_content 'Test merge request' - end - - context 'when a commit comment exists on the merge request' do - given(:comment) { 'A commit comment' } - given(:reply) { 'A reply comment' } - - background do - create(:note_on_commit, note: comment, - project: forked_project, - commit_id: merge_request.commit_shas.first) - end - - scenario 'user can reply to the comment', :js do - visit_merge_request(merge_request) - - expect(page).to have_content(comment) - - page.within('.discussion-notes') do - find('.btn-text-field').click - find('#note_note').send_keys(reply) - find('.comment-btn').click - end - - wait_for_requests - - expect(page).to have_content(reply) - end - end - - context 'source project is deleted' do - background do - MergeRequests::MergeService.new(project, user).execute(merge_request) - forked_project.destroy! - end - - scenario 'user can access merge request', :js do - visit_merge_request(merge_request) - - expect(page).to have_content 'Test merge request' - expect(page).to have_content "(removed):#{merge_request.source_branch}" - end - end - - context 'pipeline present in source project' do - given(:pipeline) do - create(:ci_pipeline, - project: forked_project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch) - end - - background do - create(:ci_build, pipeline: pipeline, name: 'rspec') - create(:ci_build, pipeline: pipeline, name: 'spinach') - end - - scenario 'user visits a pipelines page', :js do - visit_merge_request(merge_request) - page.within('.merge-request-tabs') { click_link 'Pipelines' } - - page.within('.ci-table') do - expect(page).to have_content pipeline.id - end - end - end - - def visit_merge_request(mr) - visit project_merge_request_path(project, mr) - end -end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb deleted file mode 100644 index 4362f8b3fcc..00000000000 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -feature 'Edit Merge Request' do - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, :simple, source_project: project) } - - before do - project.team << [user, :master] - - sign_in user - - visit edit_project_merge_request_path(project, merge_request) - end - - context 'editing a MR' do - it 'has class js-quick-submit in form' do - expect(page).to have_selector('.js-quick-submit') - end - - it 'warns about version conflict' do - merge_request.update(title: "New title") - - fill_in 'merge_request_title', with: 'bug 345' - fill_in 'merge_request_description', with: 'bug description' - - click_button 'Save changes' - - expect(page).to have_content 'Someone edited the merge request the same time you did' - end - - it 'allows to unselect "Remove source branch"', :js do - merge_request.update(merge_params: { 'force_remove_source_branch' => '1' }) - expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy - - visit edit_project_merge_request_path(project, merge_request) - uncheck 'Remove source branch when merge request is accepted' - - click_button 'Save changes' - - expect(page).to have_unchecked_field 'remove-source-branch-input' - expect(page).to have_content 'Remove source branch' - end - - it 'should preserve description textarea height', :js do - long_description = %q( - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat. - - Cras congue nec ligula tristique viverra. Curabitur fringilla fringilla fringilla. Donec rhoncus dignissim orci ut accumsan. Ut rutrum urna a rhoncus varius. Maecenas blandit, mauris nec accumsan gravida, augue nibh finibus magna, sed maximus turpis libero nec neque. Suspendisse at semper est. Nunc imperdiet dapibus dui, varius sollicitudin erat luctus non. Sed pellentesque ligula eget posuere facilisis. Donec dictum commodo volutpat. Donec egestas dui ac magna sollicitudin bibendum. Vivamus purus neque, ullamcorper ac feugiat et, tempus sit amet metus. Praesent quis viverra neque. Sed bibendum viverra est, eu aliquam mi ornare vitae. Proin et dapibus ipsum. Nunc tortor diam, malesuada nec interdum vel, placerat quis justo. Ut viverra at erat eu laoreet. - - Pellentesque commodo, diam sit amet dignissim condimentum, tortor justo pretium est, non venenatis metus eros ut nunc. Etiam ut neque eget sem dapibus aliquam. Curabitur vel elit lorem. Nulla nec enim elit. Sed ut ex id justo facilisis convallis at ac augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nullam cursus egestas turpis non tristique. Suspendisse in erat sem. Fusce libero elit, fermentum gravida mauris id, auctor iaculis felis. Nullam vulputate tempor laoreet. - - Nam tempor et magna sed convallis. Fusce sit amet sollicitudin risus, a ullamcorper lacus. Morbi gravida quis sem eget porttitor. Donec eu egestas mauris, in elementum tortor. Sed eget ex mi. Mauris iaculis tortor ut est auctor, nec dignissim quam sagittis. Suspendisse vel metus non quam suscipit tincidunt. Cras molestie lacus non justo finibus sodales quis vitae erat. In a porttitor nisi, id sollicitudin urna. Ut at felis tellus. Suspendisse potenti. - - Maecenas leo ligula, varius at neque vitae, ornare maximus justo. Nullam convallis luctus risus et vulputate. Duis suscipit faucibus iaculis. Etiam quis tortor faucibus, tristique tellus sit amet, sodales neque. Nulla dapibus nisi vel aliquet consequat. Etiam faucibus, metus eget condimentum iaculis, enim urna lobortis sem, id efficitur eros sapien nec nisi. Aenean ut finibus ex. - ) - - fill_in 'merge_request_description', with: long_description - - height = get_textarea_height - find('.js-md-preview-button').click - find('.js-md-write-button').click - new_height = get_textarea_height - - expect(height).to eq(new_height) - end - - def get_textarea_height - find('#merge_request_description') - page.evaluate_script('document.getElementById("merge_request_description").offsetHeight') - end - end -end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb deleted file mode 100644 index 7adae08e499..00000000000 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'rails_helper' - -feature 'Merge Request filtering by Labels', :js do - include FilteredSearchHelpers - include MergeRequestHelpers - - let(:project) { create(:project, :public, :repository) } - let!(:user) { create(:user) } - let!(:label) { create(:label, project: project) } - - let!(:bug) { create(:label, project: project, title: 'bug') } - let!(:feature) { create(:label, project: project, title: 'feature') } - let!(:enhancement) { create(:label, project: project, title: 'enhancement') } - - let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } - let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "wip") } - let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "improve/awesome") } - - before do - mr1.labels << bug - - mr2.labels << bug - mr2.labels << enhancement - - mr3.title = "Feature1" - mr3.labels << feature - - project.add_master(user) - sign_in(user) - - visit project_merge_requests_path(project) - end - - context 'filter by label bug' do - before do - input_filtered_search('label:~bug') - end - - it 'apply the filter' do - expect(page).to have_content "Bugfix1" - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - end - end - - context 'filter by label feature' do - before do - input_filtered_search('label:~feature') - end - - it 'applies the filter' do - expect(page).to have_content "Feature1" - expect(page).not_to have_content "Bugfix2" - expect(page).not_to have_content "Bugfix1" - end - end - - context 'filter by label enhancement' do - before do - input_filtered_search('label:~enhancement') - end - - it 'applies the filter' do - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(page).not_to have_content "Bugfix1" - end - end - - context 'filter by label enhancement and bug in issues list' do - before do - input_filtered_search('label:~bug label:~enhancement') - end - - it 'applies the filters' do - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - end - end - - context 'filter dropdown' do - it 'filters by label name' do - init_label_search - filtered_search.send_keys('~bug') - - page.within '.filter-dropdown' do - expect(page).not_to have_content 'enhancement' - expect(page).to have_content 'bug' - end - end - end -end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb deleted file mode 100644 index 8b9ff9be943..00000000000 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -require 'rails_helper' - -feature 'Merge Request filtering by Milestone' do - include FilteredSearchHelpers - include MergeRequestHelpers - - let(:project) { create(:project, :public, :repository) } - let!(:user) { create(:user)} - let(:milestone) { create(:milestone, project: project) } - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter a", text: title).click - end - - before do - project.team << [user, :master] - sign_in(user) - end - - scenario 'filters by no Milestone', :js do - create(:merge_request, :with_diffs, source_project: project) - create(:merge_request, :simple, source_project: project, milestone: milestone) - - visit_merge_requests(project) - input_filtered_search('milestone:none') - - expect_tokens([milestone_token('none', false)]) - expect_filtered_search_input_empty - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_css('.merge-request', count: 1) - end - - context 'filters by upcoming milestone', :js do - it 'does not show merge requests with no expiry' do - create(:merge_request, :with_diffs, source_project: project) - create(:merge_request, :simple, source_project: project, milestone: milestone) - - visit_merge_requests(project) - input_filtered_search('milestone:upcoming') - - expect(page).to have_css('.merge-request', count: 0) - end - - it 'shows merge requests in future' do - milestone = create(:milestone, project: project, due_date: Date.tomorrow) - create(:merge_request, :with_diffs, source_project: project) - create(:merge_request, :simple, source_project: project, milestone: milestone) - - visit_merge_requests(project) - input_filtered_search('milestone:upcoming') - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_css('.merge-request', count: 1) - end - - it 'does not show merge requests in past' do - milestone = create(:milestone, project: project, due_date: Date.yesterday) - create(:merge_request, :with_diffs, source_project: project) - create(:merge_request, :simple, source_project: project, milestone: milestone) - - visit_merge_requests(project) - input_filtered_search('milestone:upcoming') - - expect(page).to have_css('.merge-request', count: 0) - end - end - - scenario 'filters by a specific Milestone', :js do - create(:merge_request, :with_diffs, source_project: project, milestone: milestone) - create(:merge_request, :simple, source_project: project) - - visit_merge_requests(project) - input_filtered_search("milestone:%'#{milestone.title}'") - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_css('.merge-request', count: 1) - end - - context 'when milestone has single quotes in title' do - background do - milestone.update(name: "rock 'n' roll") - end - - scenario 'filters by a specific Milestone', :js do - create(:merge_request, :with_diffs, source_project: project, milestone: milestone) - create(:merge_request, :simple, source_project: project) - - visit_merge_requests(project) - input_filtered_search("milestone:%\"#{milestone.title}\"") - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_css('.merge-request', count: 1) - end - end -end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb deleted file mode 100644 index aac295ab940..00000000000 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ /dev/null @@ -1,337 +0,0 @@ -require 'rails_helper' - -describe 'Filter merge requests' do - include FilteredSearchHelpers - include MergeRequestHelpers - - let!(:project) { create(:project, :repository) } - let!(:group) { create(:group) } - let!(:user) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } - let!(:label) { create(:label, project: project) } - let!(:wontfix) { create(:label, project: project, title: "Won't fix") } - - before do - project.add_master(user) - group.add_developer(user) - sign_in(user) - create(:merge_request, source_project: project, target_project: project) - - visit project_merge_requests_path(project) - end - - describe 'for assignee from mr#index' do - let(:search_query) { "assignee:@#{user.username}" } - - def expect_assignee_visual_tokens - wait_for_requests - - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - before do - input_filtered_search(search_query) - - expect_mr_list_count(0) - end - - context 'assignee', :js do - it 'updates to current user' do - expect_assignee_visual_tokens() - end - - it 'does not change when closed link is clicked' do - find('.issues-state-filters [data-state="closed"]').click - - expect_assignee_visual_tokens() - end - - it 'does not change when all link is clicked' do - find('.issues-state-filters [data-state="all"]').click - - expect_assignee_visual_tokens() - end - end - end - - describe 'for milestone from mr#index' do - let(:search_query) { "milestone:%\"#{milestone.title}\"" } - - def expect_milestone_visual_tokens - expect_tokens([milestone_token("\"#{milestone.title}\"")]) - expect_filtered_search_input_empty - end - - before do - input_filtered_search(search_query) - - expect_mr_list_count(0) - end - - context 'milestone', :js do - it 'updates to current milestone' do - expect_milestone_visual_tokens() - end - - it 'does not change when closed link is clicked' do - find('.issues-state-filters [data-state="closed"]').click - - expect_milestone_visual_tokens() - end - - it 'does not change when all link is clicked' do - find('.issues-state-filters [data-state="all"]').click - - expect_milestone_visual_tokens() - end - end - end - - describe 'for label from mr#index', :js do - it 'filters by no label' do - input_filtered_search('label:none') - - expect_mr_list_count(1) - expect_tokens([label_token('none', false)]) - expect_filtered_search_input_empty - end - - it 'filters by a label' do - input_filtered_search("label:~#{label.title}") - - expect_mr_list_count(0) - expect_tokens([label_token(label.title)]) - expect_filtered_search_input_empty - end - - it "filters by `won't fix` and another label" do - input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") - - expect_mr_list_count(0) - expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) - expect_filtered_search_input_empty - end - - it "filters by `won't fix` label followed by another label after page load" do - input_filtered_search("label:~\"#{wontfix.title}\"") - - expect_mr_list_count(0) - expect_tokens([label_token("\"#{wontfix.title}\"")]) - expect_filtered_search_input_empty - - input_filtered_search_keys("label:~#{label.title}") - - expect_mr_list_count(0) - expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) - expect_filtered_search_input_empty - end - end - - describe 'for assignee and label from mr#index' do - let(:search_query) { "assignee:@#{user.username} label:~#{label.title}" } - - before do - input_filtered_search(search_query) - - expect_mr_list_count(0) - end - - context 'assignee and label', :js do - def expect_assignee_label_visual_tokens - wait_for_requests - - expect_tokens([assignee_token(user.name), label_token(label.title)]) - expect_filtered_search_input_empty - end - - it 'updates to current assignee and label' do - expect_assignee_label_visual_tokens() - end - - it 'does not change when closed link is clicked' do - find('.issues-state-filters [data-state="closed"]').click - - expect_assignee_label_visual_tokens() - end - - it 'does not change when all link is clicked' do - find('.issues-state-filters [data-state="all"]').click - - expect_assignee_label_visual_tokens() - end - end - end - - describe 'filter merge requests by text' do - before do - create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "wip") - - bug_label = create(:label, project: project, title: 'bug') - milestone = create(:milestone, title: "8", project: project) - - mr = create(:merge_request, - title: "Bug 2", - source_project: project, - target_project: project, - source_branch: "fix", - milestone: milestone, - author: user, - assignee: user) - mr.labels << bug_label - - visit project_merge_requests_path(project) - end - - context 'only text', :js do - it 'filters merge requests by searched text' do - input_filtered_search('bug') - - expect_mr_list_count(2) - end - - it 'does not show any merge requests' do - input_filtered_search('testing') - - page.within '.mr-list' do - expect(page).not_to have_selector('.merge-request') - end - end - end - - context 'filters and searches', :js do - it 'filters by text and label' do - input_filtered_search('Bug') - - expect_mr_list_count(2) - expect_filtered_search_input('Bug') - - input_filtered_search_keys(' label:~bug') - - expect_mr_list_count(1) - expect_tokens([label_token('bug')]) - expect_filtered_search_input('Bug') - end - - it 'filters by text and milestone' do - input_filtered_search('Bug') - - expect_mr_list_count(2) - expect_filtered_search_input('Bug') - - input_filtered_search_keys(' milestone:%8') - - expect_mr_list_count(1) - expect_tokens([milestone_token('8')]) - expect_filtered_search_input('Bug') - end - - it 'filters by text and assignee' do - input_filtered_search('Bug') - - expect_mr_list_count(2) - expect_filtered_search_input('Bug') - - input_filtered_search_keys(" assignee:@#{user.username}") - - expect_mr_list_count(1) - - wait_for_requests - - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input('Bug') - end - - it 'filters by text and author' do - input_filtered_search('Bug') - - expect_mr_list_count(2) - expect_filtered_search_input('Bug') - - input_filtered_search_keys(" author:@#{user.username}") - - wait_for_requests - - expect_mr_list_count(1) - expect_tokens([author_token(user.name)]) - expect_filtered_search_input('Bug') - end - end - end - - describe 'filter merge requests and sort', :js do - before do - bug_label = create(:label, project: project, title: 'bug') - - mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "wip") - mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "fix") - - mr1.labels << bug_label - mr2.labels << bug_label - - visit project_merge_requests_path(project) - end - - it 'is able to filter and sort merge requests' do - input_filtered_search('label:~bug') - - expect_mr_list_count(2) - - click_button 'Created date' - page.within '.dropdown-menu-sort' do - click_link 'Priority' - end - wait_for_requests - - page.within '.mr-list' do - expect(page).to have_content('Frontend') - end - end - end - - describe 'filter by assignee id', :js do - it 'filter by current user' do - visit project_merge_requests_path(project, assignee_id: user.id) - - wait_for_requests - - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'filter by new user' do - new_user = create(:user) - project.add_developer(new_user) - - visit project_merge_requests_path(project, assignee_id: new_user.id) - - wait_for_requests - - expect_tokens([assignee_token(new_user.name)]) - expect_filtered_search_input_empty - end - end - - describe 'filter by author id', :js do - it 'filter by current user' do - visit project_merge_requests_path(project, author_id: user.id) - - wait_for_requests - - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'filter by new user' do - new_user = create(:user) - project.add_developer(new_user) - - visit project_merge_requests_path(project, author_id: new_user.id) - - wait_for_requests - - expect_tokens([author_token(new_user.name)]) - expect_filtered_search_input_empty - end - end -end diff --git a/spec/features/merge_requests/filters_generic_behavior_spec.rb b/spec/features/merge_requests/filters_generic_behavior_spec.rb new file mode 100644 index 00000000000..0e7fac6b409 --- /dev/null +++ b/spec/features/merge_requests/filters_generic_behavior_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe 'Merge Requests > Filters generic behavior', :js do + include FilteredSearchHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:open_mr) { create(:merge_request, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') } + let(:merged_mr) { create(:merge_request, :merged, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2') } + let(:closed_mr) { create(:merge_request, :closed, title: 'Feature', source_project: project, target_project: project, source_branch: 'improve/awesome') } + + before do + open_mr.labels << bug + merged_mr.labels << bug + closed_mr.labels << bug + + sign_in(user) + visit project_merge_requests_path(project) + end + + context 'when filtered by a label' do + before do + input_filtered_search('label:~bug') + end + + describe 'state tabs' do + it 'does not change when state tabs are clicked' do + expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3) + expect(page).to have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix2' + expect(page).not_to have_content 'Feature' + + find('.issues-state-filters [data-state="merged"]').click + + expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3) + expect(page).not_to have_content 'Bugfix1' + expect(page).to have_content 'Bugfix2' + expect(page).not_to have_content 'Feature' + + find('.issues-state-filters [data-state="closed"]').click + + expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3) + expect(page).not_to have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix2' + expect(page).to have_content 'Feature' + + find('.issues-state-filters [data-state="all"]').click + + expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3) + expect(page).to have_content 'Bugfix1' + expect(page).to have_content 'Bugfix2' + expect(page).to have_content 'Feature' + end + end + + describe 'clear button' do + it 'allows user to remove filtered labels' do + first('.clear-search').click + filtered_search.send_keys(:enter) + + expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3) + expect(page).to have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix2' + expect(page).not_to have_content 'Feature' + end + end + end + + context 'filter dropdown' do + it 'filters by label name' do + init_label_search + filtered_search.send_keys('~bug') + + page.within '.filter-dropdown' do + expect(page).not_to have_content 'enhancement' + expect(page).to have_content 'bug' + end + end + end +end diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb deleted file mode 100644 index 1dcc1e139a0..00000000000 --- a/spec/features/merge_requests/form_spec.rb +++ /dev/null @@ -1,301 +0,0 @@ -require 'rails_helper' - -describe 'New/edit merge request', :js do - include ProjectForksHelper - - let!(:project) { create(:project, :public, :repository) } - let(:forked_project) { fork_project(project, nil, repository: true) } - let!(:user) { create(:user) } - let!(:user2) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } - let!(:label) { create(:label, project: project) } - let!(:label2) { create(:label, project: project) } - - before do - project.team << [user, :master] - project.team << [user2, :master] - end - - context 'owned projects' do - before do - sign_in(user) - end - - context 'new merge request' do - before do - visit project_new_merge_request_path( - project, - merge_request: { - source_project_id: project.id, - target_project_id: project.id, - source_branch: 'fix', - target_branch: 'master' - }) - end - - it 'creates new merge request' do - click_button 'Assignee' - page.within '.dropdown-menu-user' do - click_link user2.name - end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s) - page.within '.js-assignee-search' do - expect(page).to have_content user2.name - end - - find('a', text: 'Assign to me').click - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-assignee-search' do - expect(page).to have_content user.name - end - - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end - - click_button 'Labels' - page.within '.dropdown-menu-labels' do - click_link label.title - click_link label2.title - end - page.within '.js-label-select' do - expect(page).to have_content label.title - end - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - - click_button 'Submit merge request' - - page.within '.issuable-sidebar' do - page.within '.assignee' do - expect(page).to have_content user.name - end - - page.within '.milestone' do - expect(page).to have_content milestone.title - end - - page.within '.labels' do - expect(page).to have_content label.title - expect(page).to have_content label2.title - end - end - - page.within '.breadcrumbs' do - merge_request = MergeRequest.find_by(source_branch: 'fix') - - expect(page).to have_text("Merge Requests #{merge_request.to_reference}") - end - end - - it 'description has autocomplete' do - find('#merge_request_description').native.send_keys('') - fill_in 'merge_request_description', with: '@' - - expect(page).to have_selector('.atwho-view') - end - end - - context 'edit merge request' do - before do - merge_request = create(:merge_request, - source_project: project, - target_project: project, - source_branch: 'fix', - target_branch: 'master' - ) - - visit edit_project_merge_request_path(project, merge_request) - end - - it 'updates merge request' do - click_button 'Assignee' - page.within '.dropdown-menu-user' do - click_link user.name - end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-assignee-search' do - expect(page).to have_content user.name - end - - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end - - click_button 'Labels' - page.within '.dropdown-menu-labels' do - click_link label.title - click_link label2.title - end - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - page.within '.js-label-select' do - expect(page).to have_content label.title - end - - click_button 'Save changes' - - page.within '.issuable-sidebar' do - page.within '.assignee' do - expect(page).to have_content user.name - end - - page.within '.milestone' do - expect(page).to have_content milestone.title - end - - page.within '.labels' do - expect(page).to have_content label.title - expect(page).to have_content label2.title - end - end - end - - it 'description has autocomplete' do - find('#merge_request_description').native.send_keys('') - fill_in 'merge_request_description', with: '@' - - expect(page).to have_selector('.atwho-view') - end - end - end - - context 'forked project' do - before do - forked_project.team << [user, :master] - sign_in(user) - end - - context 'new merge request' do - before do - visit project_new_merge_request_path( - forked_project, - merge_request: { - source_project_id: forked_project.id, - target_project_id: project.id, - source_branch: 'fix', - target_branch: 'master' - }) - end - - it 'creates new merge request' do - click_button 'Assignee' - page.within '.dropdown-menu-user' do - click_link user.name - end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-assignee-search' do - expect(page).to have_content user.name - end - - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end - - click_button 'Labels' - page.within '.dropdown-menu-labels' do - click_link label.title - click_link label2.title - end - page.within '.js-label-select' do - expect(page).to have_content label.title - end - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - - click_button 'Submit merge request' - - page.within '.issuable-sidebar' do - page.within '.assignee' do - expect(page).to have_content user.name - end - - page.within '.milestone' do - expect(page).to have_content milestone.title - end - - page.within '.labels' do - expect(page).to have_content label.title - expect(page).to have_content label2.title - end - end - end - end - - context 'edit merge request' do - before do - merge_request = create(:merge_request, - source_project: forked_project, - target_project: project, - source_branch: 'fix', - target_branch: 'master' - ) - - visit edit_project_merge_request_path(project, merge_request) - end - - it 'should update merge request' do - click_button 'Assignee' - page.within '.dropdown-menu-user' do - click_link user.name - end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-assignee-search' do - expect(page).to have_content user.name - end - - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end - - click_button 'Labels' - page.within '.dropdown-menu-labels' do - click_link label.title - click_link label2.title - end - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) - expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - page.within '.js-label-select' do - expect(page).to have_content label.title - end - - click_button 'Save changes' - - page.within '.issuable-sidebar' do - page.within '.assignee' do - expect(page).to have_content user.name - end - - page.within '.milestone' do - expect(page).to have_content milestone.title - end - - page.within '.labels' do - expect(page).to have_content label.title - expect(page).to have_content label2.title - end - end - end - end - end -end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb deleted file mode 100644 index eed95816bdf..00000000000 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -require 'rails_helper' - -feature 'Merge requests filter clear button', :js do - include FilteredSearchHelpers - include MergeRequestHelpers - include IssueHelpers - - let!(:project) { create(:project, :public, :repository) } - let!(:user) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } - let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "improve/awesome", milestone: milestone, author: user, assignee: user) } - let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } - - let(:merge_request_css) { '.merge-request' } - let(:clear_search_css) { '.filtered-search-box .clear-search' } - - before do - mr2.labels << bug - project.team << [user, :developer] - end - - context 'when a milestone filter has been applied' do - it 'resets the milestone filter' do - visit_merge_requests(project, milestone_title: milestone.title) - - expect(page).to have_css(merge_request_css, count: 1) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when a label filter has been applied' do - it 'resets the label filter' do - visit_merge_requests(project, label_name: bug.name) - - expect(page).to have_css(merge_request_css, count: 1) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when multiple label filters have been applied' do - let!(:label) { create(:label, project: project, name: 'Frontend') } - let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") } - - before do - visit_merge_requests(project) - init_label_search - end - - it 'filters bug label' do - filtered_search.set('~bug') - - filter_dropdown.find('.filter-dropdown-item', text: bug.title).click - init_label_search - - expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible - expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible - end - end - - context 'when a text search has been conducted' do - it 'resets the text search filter' do - visit_merge_requests(project, search: 'Bug') - - expect(page).to have_css(merge_request_css, count: 1) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when author filter has been applied' do - it 'resets the author filter' do - visit_merge_requests(project, author_username: user.username) - - expect(page).to have_css(merge_request_css, count: 1) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when assignee filter has been applied' do - it 'resets the assignee filter' do - visit_merge_requests(project, assignee_username: user.username) - - expect(page).to have_css(merge_request_css, count: 1) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when all filters have been applied' do - it 'clears all filters' do - visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') - - expect(page).to have_css(merge_request_css, count: 0) - expect(get_filtered_search_placeholder).to eq('') - - reset_filters - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - end - end - - context 'when no filters have been applied' do - it 'the clear button should not be visible' do - visit_merge_requests(project) - - expect(page).to have_css(merge_request_css, count: 2) - expect(get_filtered_search_placeholder).to eq(default_placeholder) - expect(page).not_to have_css(clear_search_css) - end - end -end diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb deleted file mode 100644 index bce36e05e57..00000000000 --- a/spec/features/merge_requests/target_branch_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe 'Target branch', :js do - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request) } - let(:project) { merge_request.project } - - def path_to_merge_request - project_merge_request_path(project, merge_request) - end - - before do - sign_in user - project.team << [user, :master] - end - - context 'when branch was deleted' do - before do - DeleteBranchService.new(project, user).execute('feature') - visit path_to_merge_request - end - - it 'shows a message about missing target branch' do - expect(page).to have_content( - 'Target branch does not exist' - ) - end - - it 'does not show link to target branch' do - expect(page).not_to have_selector('.mr-widget-body .js-branch-text a') - end - end -end diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb deleted file mode 100644 index cd92ad22267..00000000000 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -feature 'toggler_behavior', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:merge_request) { create(:merge_request, source_project: project, author: user) } - let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } - let(:fragment_id) { "#note_#{note.id}" } - - before do - sign_in(create(:admin)) - project = merge_request.source_project - page.current_window.resize_to(1000, 300) - visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}" - end - - describe 'scroll position' do - it 'should be scrolled down to fragment' do - page_height = page.current_window.size[1] - page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)") - expect(find('.js-toggle-content').visible?).to eq true - expect(find(fragment_id).visible?).to eq true - expect(fragment_position_top).to be >= page_scroll_y - expect(fragment_position_top).to be < (page_scroll_y + page_height) - end - end -end diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb new file mode 100644 index 00000000000..d6c770c93f1 --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe 'Merge Requests > User filters by assignees', :js do + include FilteredSearchHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + + before do + create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') + create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2') + + sign_in(user) + visit project_merge_requests_path(project) + end + + context 'filtering by assignee:none' do + it 'applies the filter' do + input_filtered_search('assignee:none') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).not_to have_content 'Bugfix1' + expect(page).to have_content 'Bugfix2' + end + end + + context 'filtering by assignee:@username' do + it 'applies the filter' do + input_filtered_search("assignee:@#{user.username}") + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix2' + end + end +end diff --git a/spec/features/merge_requests/user_filters_by_labels_spec.rb b/spec/features/merge_requests/user_filters_by_labels_spec.rb new file mode 100644 index 00000000000..08d741af93d --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_labels_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +describe 'Merge Requests > User filters by labels', :js do + include FilteredSearchHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:mr1) { create(:merge_request, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') } + let(:mr2) { create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2') } + + before do + bug_label = create(:label, project: project, title: 'bug') + enhancement_label = create(:label, project: project, title: 'enhancement') + mr1.labels << bug_label + mr2.labels << bug_label << enhancement_label + + sign_in(user) + visit project_merge_requests_path(project) + end + + context 'filtering by label:none' do + it 'applies the filter' do + input_filtered_search('label:none') + + expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) + expect(page).not_to have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix2' + end + end + + context 'filtering by label:~enhancement' do + it 'applies the filter' do + input_filtered_search('label:~enhancement') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content 'Bugfix2' + expect(page).not_to have_content 'Bugfix1' + end + end + + context 'filtering by label:~enhancement and label:~bug' do + it 'applies the filters' do + input_filtered_search('label:~bug label:~enhancement') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content 'Bugfix2' + end + end +end diff --git a/spec/features/merge_requests/user_filters_by_milestones_spec.rb b/spec/features/merge_requests/user_filters_by_milestones_spec.rb new file mode 100644 index 00000000000..727a236d980 --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_milestones_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe 'Merge Requests > User filters by milestones', :js do + include FilteredSearchHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:milestone) { create(:milestone, project: project) } + + before do + create(:merge_request, :with_diffs, source_project: project) + create(:merge_request, :simple, source_project: project, milestone: milestone) + + sign_in(user) + visit project_merge_requests_path(project) + end + + it 'filters by no milestone' do + input_filtered_search('milestone:none') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_css('.merge-request', count: 1) + end + + it 'filters by a specific milestone' do + input_filtered_search("milestone:%'#{milestone.title}'") + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_css('.merge-request', count: 1) + end + + describe 'filters by upcoming milestone' do + it 'does not show merge requests with no expiry' do + input_filtered_search('milestone:upcoming') + + expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) + expect(page).to have_css('.merge-request', count: 0) + end + + context 'with an upcoming milestone' do + let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) } + + it 'shows merge requests' do + input_filtered_search('milestone:upcoming') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_css('.merge-request', count: 1) + end + end + + context 'with a due milestone' do + let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) } + + it 'does not show any merge requests' do + input_filtered_search('milestone:upcoming') + + expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) + expect(page).to have_css('.merge-request', count: 0) + end + end + end +end diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb new file mode 100644 index 00000000000..1615899a047 --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe 'Merge requests > User filters by multiple criteria', :js do + include FilteredSearchHelpers + + let!(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let!(:milestone) { create(:milestone, title: 'v1.1', project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + before do + sign_in(user) + mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone) + mr.labels << wontfix + + visit project_merge_requests_path(project) + end + + describe 'filtering by label:~"Won\'t fix" and assignee:~bug' do + it 'applies the filters' do + input_filtered_search("label:~\"Won't fix\" assignee:@#{user.username}") + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content 'Bugfix2' + expect_filtered_search_input_empty + end + end + + describe 'filtering by text, author, assignee, milestone, and label' do + it 'filters by text, author, assignee, milestone, and label' do + input_filtered_search_keys("author:@#{user.username} assignee:@#{user.username} milestone:%\"v1.1\" label:~\"Won't fix\" Bug") + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content 'Bugfix2' + expect_filtered_search_input('Bug') + end + end +end diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 416a0f78a45..ef7ae490b0f 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -1,6 +1,6 @@ -require 'spec_helper' +require 'rails_helper' -describe 'Projects > Merge requests > User lists merge requests' do +describe 'Merge requests > User lists merge requests' do include MergeRequestHelpers include SortingHelper diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index c5498563b39..199ba7e87ad 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' -feature 'Multiple merge requests updating from merge_requests#index' do - let!(:user) { create(:user)} - let!(:project) { create(:project, :repository) } +describe 'Merge requests > User mass updates', :js do + let(:project) { create(:project, :repository) } + let(:user) { project.creator } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end - context 'status', :js do + context 'status' do describe 'close merge request' do before do visit project_merge_requests_path(project) @@ -37,13 +37,13 @@ feature 'Multiple merge requests updating from merge_requests#index' do end end - context 'assignee', :js do + context 'assignee' do describe 'set assignee' do before do visit project_merge_requests_path(project) end - it "updates merge request with assignee" do + it 'updates merge request with assignee' do change_assignee(user.name) page.within('.merge-request .controls') do @@ -59,7 +59,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do visit project_merge_requests_path(project) end - it "removes assignee from the merge request" do + it 'removes assignee from the merge request' do change_assignee('Unassigned') expect(find('.merge-request .controls')).not_to have_css('.author_link') @@ -67,7 +67,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end end - context 'milestone', :js do + context 'milestone' do let(:milestone) { create(:milestone, project: project) } describe 'set milestone' do @@ -75,7 +75,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do visit project_merge_requests_path(project) end - it "updates merge request with milestone" do + it 'updates merge request with milestone' do change_milestone(milestone.title) expect(find('.merge-request')).to have_content milestone.title @@ -89,7 +89,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do visit project_merge_requests_path(project) end - it "removes milestone from the merge request" do + it 'removes milestone from the merge request' do change_milestone("No Milestone") expect(find('.merge-request')).not_to have_content milestone.title diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb deleted file mode 100644 index 72a52c979b3..00000000000 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'spec_helper' - -feature 'Widget Deployments Header', :js do - describe 'when deployed to an environment' do - given(:user) { create(:user) } - given(:project) { merge_request.target_project } - given(:merge_request) { create(:merge_request, :merged) } - given(:environment) { create(:environment, project: project) } - given(:role) { :developer } - given(:sha) { project.commit('master').id } - given!(:deployment) { create(:deployment, environment: environment, sha: sha) } - given!(:manual) { } - - background do - sign_in(user) - project.team << [user, role] - visit project_merge_request_path(project, merge_request) - end - - scenario 'displays that the environment is deployed' do - wait_for_requests - - expect(page).to have_content("Deployed to #{environment.name}") - expect(find('.js-deploy-time')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) - end - - context 'with stop action' do - given(:pipeline) { create(:ci_pipeline, project: project) } - given(:build) { create(:ci_build, pipeline: pipeline) } - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) do - create(:deployment, environment: environment, ref: merge_request.target_branch, - sha: sha, deployable: build, on_stop: 'close_app') - end - - background do - wait_for_requests - end - - scenario 'does show stop button' do - expect(page).to have_button('Stop environment') - end - - scenario 'does start build when stop button clicked' do - accept_confirm { click_button('Stop environment') } - - expect(page).to have_content('close_app') - end - - context 'for reporter' do - given(:role) { :reporter } - - scenario 'does not show stop button' do - expect(page).not_to have_button('Stop environment') - end - end - end - end -end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 9f24193a2ac..b02d2d4261c 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -7,7 +7,7 @@ feature 'Milestone' do before do create(:group_member, group: group, user: user) - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 49d8e52f861..a5e325ee2e3 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -10,8 +10,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do def stub_omniauth_config(provider) OmniAuth.config.add_mock(provider, OmniAuth::AuthHash.new(provider: provider.to_s, uid: "12345")) - set_devise_mapping(context: Rails.application) - Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + stub_omniauth_provider(provider) end providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index b45972b7f6b..73a526c3d8a 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -33,6 +33,25 @@ feature 'Password reset' do end end + describe 'Changing password while logged in' do + it 'updates the password' do + user = create(:user) + token = user.send_reset_password_instructions + + sign_in(user) + + visit(edit_user_password_path(reset_password_token: token)) + + fill_in 'New password', with: 'hello1234' + fill_in 'Confirm new password', with: 'hello1234' + + click_button 'Change your password' + + expect(page).to have_content(I18n.t('devise.passwords.updated_not_active')) + expect(current_path).to eq new_user_session_path + end + end + def forgot_password(user) visit root_path click_on 'Forgot your password?' diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index 7d5ba3a7328..b04a5422fed 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -27,6 +27,7 @@ feature 'Profile > SSH Keys' do expect(page).to have_content("Title: #{attrs[:title]}") expect(page).to have_content(attrs[:key]) + expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title]) end context 'when only DSA and ECDSA keys are allowed' do diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index d1edeef8da4..7d204f89fba 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -2,12 +2,20 @@ require 'spec_helper' describe 'Profile > Applications' do let(:user) { create(:user) } + let(:application) { create(:oauth_application, owner: user) } before do sign_in(user) end describe 'User manages applications', :js do + it 'views an application' do + visit oauth_application_path(application) + + expect(page).to have_content("Application: #{application.name}") + expect(find('.breadcrumbs-sub-title')).to have_link(application.name) + end + it 'deletes an application' do create(:oauth_application, owner: user) visit oauth_applications_path diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index df89918f17a..1952fdae798 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -5,7 +5,7 @@ feature 'User visits the notifications tab', :js do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit(profile_notifications_path) end diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb index 84c2faa2015..2693e539268 100644 --- a/spec/features/projects/activity/rss_spec.rb +++ b/spec/features/projects/activity/rss_spec.rb @@ -11,7 +11,7 @@ feature 'Project Activity RSS' do context 'when signed in' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit path end diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index c68e10a2563..f51001edcd7 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -6,7 +6,7 @@ feature 'test coverage badge' do context 'when user has access to view badge' do background do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) end @@ -18,7 +18,7 @@ feature 'test coverage badge' do show_test_coverage_badge - expect_coverage_badge('95%') + expect_coverage_badge('95.00%') end scenario 'user requests coverage badge for specific job' do @@ -30,7 +30,7 @@ feature 'test coverage badge' do show_test_coverage_badge(job: 'coverage') - expect_coverage_badge('85%') + expect_coverage_badge('85.00%') end scenario 'user requests coverage badge for pipeline without coverage' do diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 68c4a647958..c705e479690 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -4,7 +4,7 @@ feature 'list of badges' do background do user = create(:user) project = create(:project, :repository) - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_pipelines_settings_path(project) end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 965028a6f90..89d3bd24b89 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -13,16 +13,19 @@ feature 'Editing file blob', :js do let(:role) { :developer } before do - project.team << [user, role] + project.add_role(user, role) sign_in(user) end - def edit_and_commit + def edit_and_commit(commit_changes: true) wait_for_requests find('.js-edit-blob').click find('#editor') execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') - click_button 'Commit changes' + + if commit_changes + click_button 'Commit changes' + end end context 'from MR diff' do @@ -39,13 +42,26 @@ feature 'Editing file blob', :js do context 'from blob file path' do before do visit project_blob_path(project, tree_join(branch, file_path)) - edit_and_commit end it 'updates content' do + edit_and_commit + expect(page).to have_content 'successfully committed' expect(page).to have_content 'NextFeature' end + + it 'previews content' do + edit_and_commit(commit_changes: false) + click_link 'Preview changes' + wait_for_requests + + old_line_count = page.all('.line_holder.old').size + new_line_count = page.all('.line_holder.new').size + + expect(old_line_count).to be > 0 + expect(new_line_count).to be > 0 + end end end @@ -55,7 +71,7 @@ feature 'Editing file blob', :js do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) visit project_edit_blob_path(project, tree_join(branch, file_path)) end @@ -90,7 +106,7 @@ feature 'Editing file blob', :js do let(:protected_branch) { 'protected-branch' } before do - project.team << [user, :developer] + project.add_developer(user) project.repository.add_branch(user, protected_branch, 'master') create(:protected_branch, project: project, name: protected_branch) sign_in(user) @@ -122,7 +138,7 @@ feature 'Editing file blob', :js do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_edit_blob_path(project, tree_join(branch, file_path)) end diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index 2f407b13c2f..39bcea013e7 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -23,7 +23,7 @@ feature 'Download buttons in branches page' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end describe 'when checking branches' do diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 7a77df83034..2fddd274078 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -8,7 +8,7 @@ describe 'Branches' do context 'logged in as developer' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end describe 'Initial branches page' do @@ -78,7 +78,7 @@ describe 'Branches' do context 'logged in as master' do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'Initial branches page' do diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index b34cd061ec6..9c4abec115f 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -73,7 +73,7 @@ feature 'Clusters Applications', :js do before do allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) - create(:cluster_applications_helm, :installed, cluster: cluster) + create(:clusters_applications_helm, :installed, cluster: cluster) page.within('.js-cluster-application-row-ingress') do page.find(:css, '.js-cluster-application-install-button').click diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 67b8901f8fb..94bde723e2f 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -13,6 +13,8 @@ feature 'Gcp Cluster', :js do end context 'when user has signed with Google' do + let(:project_id) { 'test-project-1234' } + before do allow_any_instance_of(Projects::Clusters::GcpController) .to receive(:token_in_session).and_return('token') @@ -20,105 +22,152 @@ feature 'Gcp Cluster', :js do .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end - context 'when user does not have a cluster and visits cluster index page' do + context 'when user has a GCP project with billing enabled' do before do - visit project_clusters_path(project) - - click_link 'Add cluster' - click_link 'Create on GKE' + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true') end - context 'when user filled form with valid parameters' do + context 'when user does not have a cluster and visits cluster index page' do before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_create) do - OpenStruct.new( - self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', - status: 'RUNNING' - ) + visit project_clusters_path(project) + + click_link 'Add cluster' + click_link 'Create on GKE' + end + + context 'when user filled form with valid parameters' do + before do + allow_any_instance_of(GoogleApi::CloudPlatform::Client) + .to receive(:projects_zones_clusters_create) do + OpenStruct.new( + self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', + status: 'RUNNING' + ) + end + + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create cluster' end - allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + it 'user sees a cluster details page and creation status' do + expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create cluster' - end + Clusters::Cluster.last.provider.make_created! - it 'user sees a cluster details page and creation status' do - expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') + expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine') + end - Clusters::Cluster.last.provider.make_created! + it 'user sees a error if something worng during creation' do + expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') - expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine') - end + Clusters::Cluster.last.provider.make_errored!('Something wrong!') - it 'user sees a error if something worng during creation' do - expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') + expect(page).to have_content('Something wrong!') + end + end - Clusters::Cluster.last.provider.make_errored!('Something wrong!') + context 'when user filled form with invalid parameters' do + before do + click_button 'Create cluster' + end - expect(page).to have_content('Something wrong!') + it 'user sees a validation error' do + expect(page).to have_css('#error_explanation') + end end end - context 'when user filled form with invalid parameters' do + context 'when user does have a cluster and visits cluster page' do + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + before do - click_button 'Create cluster' + visit project_cluster_path(project, cluster) end - it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + it 'user sees a cluster details page' do + expect(page).to have_button('Save changes') + expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) end - end - end - context 'when user does have a cluster and visits cluster page' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + context 'when user disables the cluster' do + before do + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click + page.within('#cluster-integration') { click_button 'Save changes' } + end - before do - visit project_cluster_path(project, cluster) - end + it 'user sees the successful message' do + expect(page).to have_content('Cluster was successfully updated.') + end + end - it 'user sees a cluster details page' do - expect(page).to have_button('Save') - expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) - end + context 'when user changes cluster parameters' do + before do + fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' + page.within('#js-cluster-details') { click_button 'Save changes' } + end - context 'when user disables the cluster' do - before do - page.find(:css, '.js-toggle-cluster').click - click_button 'Save' + it 'user sees the successful message' do + expect(page).to have_content('Cluster was successfully updated.') + expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') + end end - it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') + context 'when user destroy the cluster' do + before do + page.accept_confirm do + click_link 'Remove integration' + end + end + + it 'user sees creation form with the successful message' do + expect(page).to have_content('Cluster integration was successfully removed.') + expect(page).to have_link('Add cluster') + end end end + end - context 'when user changes cluster parameters' do - before do - fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' - end + context 'when user does not have a GCP project with billing enabled' do + before do + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('false') - it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') - expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') - end + visit project_clusters_path(project) + + click_link 'Add cluster' + click_link 'Create on GKE' + + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create cluster' end - context 'when user destroy the cluster' do - before do - page.accept_confirm do - click_link 'Remove integration' - end - end + it 'user sees form with error' do + expect(page).to have_content('Please enable billing for one of your projects to be able to create a cluster, then try again.') + end + end - it 'user sees creation form with the successful message' do - expect(page).to have_content('Cluster integration was successfully removed.') - expect(page).to have_link('Add cluster') - end + context 'when gcp billing status is not in redis' do + before do + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil) + + visit project_clusters_path(project) + + click_link 'Add cluster' + click_link 'Create on GKE' + + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create cluster' + end + + it 'user sees form with error' do + expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.') end end end diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb index 01f9526608f..3ddb35c755c 100644 --- a/spec/features/projects/clusters/interchangeability_spec.rb +++ b/spec/features/projects/clusters/interchangeability_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do - EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze + EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url= deprecated? deprecation_message].freeze EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 414f4acba86..b9ab434c259 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -29,7 +29,7 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_content('Enable cluster integration') + expect(page).to have_content('Cluster integration') expect(page.find_field('cluster[name]').value).to eq('dev-cluster') expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) .to have_content('http://example.com') @@ -57,14 +57,14 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_button('Save') + expect(page).to have_button('Save changes') end context 'when user disables the cluster' do before do - page.find(:css, '.js-toggle-cluster').click + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Save' + page.within('#cluster-integration') { click_button 'Save changes' } end it 'user sees the successful message' do @@ -76,7 +76,7 @@ feature 'User Cluster', :js do before do fill_in 'cluster_name', with: 'my-dev-cluster' fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' + page.within('#js-cluster-details') { click_button 'Save changes' } end it 'user sees the successful message' do diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 93929bf6814..497a50bebe4 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -37,13 +37,13 @@ feature 'Clusters', :js do context 'inline update of cluster' do it 'user can update cluster' do - expect(page).to have_selector('.js-toggle-cluster-list') + expect(page).to have_selector('.js-project-feature-toggle') end context 'with sucessfull request' do it 'user sees updated cluster' do expect do - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click wait_for_requests end.to change { cluster.reload.enabled } @@ -57,7 +57,7 @@ feature 'Clusters', :js do expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false } - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click expect(page).to have_content('Something went wrong on our end.') expect(page).to have_selector('.is-checked') @@ -77,4 +77,18 @@ feature 'Clusters', :js do end end end + + context 'when user has not signed in Google' do + before do + visit project_clusters_path(project) + + click_link 'Add cluster' + click_link 'Create on GKE' + end + + it 'user sees a login page' do + expect(page).to have_css('.signin-with-google') + expect(page).to have_link('Google account') + end + end end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 79e84a4f0a6..36a746ac83d 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -5,7 +5,7 @@ feature 'project commit pipelines', :js do background do user = create(:user) - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index c11a95732b2..c4c399e3058 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -9,7 +9,7 @@ describe 'Cherry-pick Commits' do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) visit project_commit_path(project, master_pickable_commit.id) end diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb index db958346f06..0d9c7355ddd 100644 --- a/spec/features/projects/commits/rss_spec.rb +++ b/spec/features/projects/commits/rss_spec.rb @@ -7,7 +7,7 @@ feature 'Project Commits RSS' do context 'when signed in' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit path end diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 41f3c15a94c..b650c1f4197 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'User broweses commits' do +describe 'User browses commits' do let(:user) { create(:user) } let(:project) { create(:project, :repository, namespace: user.namespace) } @@ -31,6 +31,19 @@ describe 'User broweses commits' do check_author_link(RepoHelpers.sample_commit.author_email, user) end end + + context 'when the blob does not exist' do + let(:commit) { create(:commit, project: project) } + + it 'shows a blank label' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + allow_any_instance_of(Gitlab::Diff::File).to receive(:raw_binary?).and_return(true) + + visit(project_commit_path(project, commit)) + + expect(find('.diff-file-changes', visible: false)).to have_content('No file name available') + end + end end private diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 87ffc2a0b90..1fb22fd0e4c 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -5,7 +5,7 @@ describe "Compare", :js do let(:project) { create(:project, :repository) } before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_compare_index_path(project, from: "master", to: "master") end diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index e445758cb5e..886c56e7163 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -5,7 +5,7 @@ describe 'Project deploy keys', :js do let(:project) { create(:project_empty_repo) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index 36809240f76..bf55917bf4c 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -5,7 +5,7 @@ feature 'Developer views empty project instructions' do let(:developer) { create(:user) } background do - project.team << [developer, :developer] + project.add_developer(developer) sign_in(developer) end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index 7a372757523..1d4b4d0fdca 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -7,7 +7,7 @@ feature 'Project edit', :js do context 'feature visibility' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit edit_project_path(project) diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index dfcf97ad495..64e600144e0 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -7,7 +7,7 @@ feature 'Environment' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end feature 'environment details page' do diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 4a05313c14a..5248a783db4 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -6,7 +6,7 @@ feature 'Environments page', :js do given(:role) { :developer } background do - project.team << [user, role] + project.add_role(user, role) sign_in(user) end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 033c45a60bf..b0eb7c5b42a 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -8,7 +8,7 @@ describe 'Edit Project Settings' do describe 'project features visibility selectors', :js do before do - project.team << [member, :master] + project.add_master(member) sign_in(member) end @@ -165,7 +165,7 @@ describe 'Edit Project Settings' do describe 'repository visibility', :js do before do - project.team << [member, :master] + project.add_master(member) sign_in(member) visit edit_project_path(project) end @@ -261,7 +261,7 @@ describe 'Edit Project Settings' do let!(:project) { create(:project, :private) } before do - project.team << [member, :guest] + project.add_guest(member) sign_in(member) visit project_path(project) end diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index 84197e45dcb..2c38c380d9d 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -5,7 +5,7 @@ feature 'user browses project', :js do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_tree_path(project, project.default_branch) end diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index e1852a6e544..8d982636525 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -5,7 +5,7 @@ feature 'User wants to create a file' do let(:user) { create(:user) } background do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_new_blob_path(project, project.default_branch) end diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index 3c3a5326538..f4a39e331fd 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -5,7 +5,7 @@ feature 'User wants to add a Dockerfile file' do before do user = create(:user) project = create(:project, :repository) - project.team << [user, :master] + project.add_master(user) sign_in user diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb index d2382d55c0b..2101627f324 100644 --- a/spec/features/projects/files/download_buttons_spec.rb +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -23,7 +23,7 @@ feature 'Download buttons in files tree' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end describe 'when files tree' do diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb index 3ab43b3c656..8d32ada5795 100644 --- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -4,7 +4,7 @@ feature 'User uses soft wrap whilst editing file', :js do before do user = create(:user) project = create(:project, :repository) - project.team << [user, :master] + project.add_master(user) sign_in user visit project_new_blob_path(project, 'master', file_name: 'test_file-name') page.within('.file-editor.code') do diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index 20be968e89f..d874cdbff8d 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -16,7 +16,7 @@ feature 'User wants to edit a file' do end background do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_edit_blob_path(project, File.join(project.default_branch, '.gitignore')) diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb index 702b99de733..ead9f7e9168 100644 --- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -5,7 +5,7 @@ feature 'User views files page' do let(:project) { create(:forked_project_with_submodules) } before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_tree_path(project, project.repository.root_ref) end diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb index 618725ee781..e9ff06c72d8 100644 --- a/spec/features/projects/files/find_file_keyboard_spec.rb +++ b/spec/features/projects/files/find_file_keyboard_spec.rb @@ -5,7 +5,7 @@ feature 'Find file keyboard shortcuts', :js do let(:project) { create(:project, :repository) } before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_find_file_path(project, project.repository.root_ref) diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index 81d68c3d67c..79f3fd09b48 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -4,7 +4,7 @@ feature 'User wants to add a .gitignore file' do before do user = create(:user) project = create(:project, :repository) - project.team << [user, :master] + project.add_master(user) sign_in user visit project_new_blob_path(project, 'master', file_name: '.gitignore') end diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb index 8e58fa7bd56..db6c67b802e 100644 --- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -4,7 +4,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do before do user = create(:user) project = create(:project, :repository) - project.team << [user, :master] + project.add_master(user) sign_in user visit project_new_blob_path(project, 'master', file_name: '.gitlab-ci.yml') end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 6c5b1086ec1..07599600876 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,7 @@ feature 'project owner creates a license file', :js do background do project.repository.delete_file(project_master, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') - project.team << [project_master, :master] + project.add_master(project_master) sign_in(project_master) visit project_path(project) end diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb index f95a60e5194..97408a9c41e 100644 --- a/spec/features/projects/files/template_type_dropdown_spec.rb +++ b/spec/features/projects/files/template_type_dropdown_spec.rb @@ -5,7 +5,7 @@ feature 'Template type dropdown selector', :js do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in user end diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index 64fe350f3dc..fbf35fb4e1c 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -5,7 +5,7 @@ feature 'Template Undo Button', :js do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in user end diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index 98c7ef57a51..199682b943c 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -5,7 +5,7 @@ describe 'Guest navigation menu' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) sign_in(guest) end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 461aa39d0ad..6732cf61767 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' # Integration test that exports a file using the Import/Export feature # It looks up for any sensitive word inside the JSON, so if a sensitive word is found -# we''l have to either include it adding the model that includes it to the +safe_list+ +# we'll have to either include it adding the model that includes it to the +safe_list+ # or make sure the attribute is blacklisted in the +import_export.yml+ configuration feature 'Import/Export - project export integration test', :js do include Select2Helper diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index af125e1b9d3..e8bb9c6a86c 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -32,7 +32,7 @@ feature 'Import/Export - project import integration test', :js do expect(page).to have_content('Import an exported GitLab project') expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}\z/).and_call_original attach_file('file', file) click_on 'Import project' diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index e76bc6f1220..c1fccf4a40b 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -1,44 +1,37 @@ require 'spec_helper' feature 'Import/Export - Namespace export file cleanup', :js do - let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } - let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') } - let(:project) { create(:project) } - - background do - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + before do + allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end after do FileUtils.rm_rf(export_path, secure: true) end - context 'admin user' do + shared_examples_for 'handling project exports on namespace change' do + let!(:old_export_path) { project.export_path } + before do sign_in(create(:admin)) + + setup_export_project end context 'moving the namespace' do - scenario 'removes the export file' do - setup_export_project - - old_export_path = project.export_path.dup - + it 'removes the export file' do expect(File).to exist(old_export_path) - project.namespace.update(path: 'new_path') + project.namespace.update!(path: build(:namespace).path) expect(File).not_to exist(old_export_path) end end context 'deleting the namespace' do - scenario 'removes the export file' do - setup_export_project - - old_export_path = project.export_path.dup - + it 'removes the export file' do expect(File).to exist(old_export_path) project.namespace.destroy @@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do expect(File).not_to exist(old_export_path) end end + end - def setup_export_project - visit edit_project_path(project) + describe 'legacy storage' do + let(:project) { create(:project) } - expect(page).to have_content('Export project') + it_behaves_like 'handling project exports on namespace change' + end + + describe 'hashed storage' do + let(:project) { create(:project, :hashed) } - find(:link, 'Export project').send_keys(:return) + it_behaves_like 'handling project exports on namespace change' + end - visit edit_project_path(project) + def setup_export_project + visit edit_project_path(project) - expect(page).to have_content('Download export') - end + expect(page).to have_content('Export project') + + find(:link, 'Export project').send_keys(:return) + + visit edit_project_path(project) + + expect(page).to have_content('Download export') end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 0c354298433..0cc68aff494 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 4319fc2746c..e26caf1f456 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -8,7 +8,7 @@ feature 'issuable templates', :js do let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' } before do - project.team << [user, :master] + project.add_master(user) sign_in user end @@ -120,7 +120,7 @@ feature 'issuable templates', :js do background do sign_out(:user) - project.team << [fork_user, :developer] + project.add_developer(fork_user) sign_in(fork_user) diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb index 58eeef8c258..ff91aabc311 100644 --- a/spec/features/projects/issues/rss_spec.rb +++ b/spec/features/projects/issues/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project Issues RSS' do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit path end diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 5d9208ebadd..4c49cff30d4 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'User browses a job', :js do - let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let!(:build) { create(:ci_build, :running, :coverage, pipeline: pipeline) } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } let(:project) { create(:project, :repository, namespace: user.namespace) } let(:user) { create(:user) } diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 0b0d5a2dce8..e661db1809a 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -15,7 +15,7 @@ feature 'Jobs' do end before do - project.team << [user, user_access_level] + project.add_role(user, user_access_level) sign_in(user) end @@ -369,6 +369,56 @@ feature 'Jobs' do end end end + + context 'Playable manual action' do + let(:job) { create(:ci_build, :playable, pipeline: pipeline) } + + before do + project.add_developer(user) + visit project_job_path(project, job) + end + + it 'shows manual action empty state' do + expect(page).to have_content('This job requires a manual action') + expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments') + expect(page).to have_link('Trigger this manual action') + end + + it 'plays manual action and shows pending status', :js do + click_link 'Trigger this manual action' + + wait_for_requests + expect(page).to have_content('This job has not started yet') + expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner') + expect(page).to have_content('pending') + end + end + + context 'Non triggered job' do + let(:job) { create(:ci_build, :created, pipeline: pipeline) } + + before do + visit project_job_path(project, job) + end + + it 'shows empty state' do + expect(page).to have_content('This job has not been triggered yet') + expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered') + end + end + + context 'Pending job' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline) } + + before do + visit project_job_path(project, job) + end + + it 'shows pending empty state' do + expect(page).to have_content('This job has not started yet') + expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner') + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index e8c70dec854..70e8d436dcb 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -9,7 +9,7 @@ feature 'Labels subscription' do context 'when signed in' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in user end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index d063f5c27b5..85bd776932b 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -12,7 +12,7 @@ feature 'Prioritize labels' do context 'when user belongs to project team' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in user end diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 3f2579bb01a..81f08e44cf3 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -23,7 +23,7 @@ feature 'Download buttons in project main page' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end describe 'when checking project main page' do diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb index 7914180b951..3c98c11b490 100644 --- a/spec/features/projects/main/rss_spec.rb +++ b/spec/features/projects/main/rss_spec.rb @@ -7,7 +7,7 @@ feature 'Project RSS' do context 'when signed in' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit path end diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb index bf0990d675d..e2a48bfd1d4 100644 --- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -6,7 +6,7 @@ feature 'Projects > Members > Anonymous user sees members' do let(:project) { create(:project, :public) } background do - project.team << [user, :master] + project.add_master(user) create(:project_group_link, project: project, group: group) end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index c140fece41d..e22b6fa6c43 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -11,7 +11,7 @@ feature 'Projects members' do let(:group_requester) { create(:user) } background do - project.team << [developer, :developer] + project.add_developer(developer) group.add_owner(user) sign_in(user) end diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index 7f067aadec6..e6d0c6e00f8 100644 --- a/spec/features/projects/members/groups_with_access_list_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -6,7 +6,7 @@ feature 'Projects > Members > Groups with access list', :js do let(:project) { create(:project, :public) } background do - project.team << [user, :master] + project.add_master(user) @group_link = create(:project_group_link, project: project, group: group) sign_in(user) diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 0f88f4cb1e8..8fe340d3bae 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -9,7 +9,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do let!(:new_member) { create(:user) } background do - project.team << [master, :master] + project.add_master(master) sign_in(master) end diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index eb3c8034873..d575596937d 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -7,7 +7,7 @@ feature 'Projects > Members > Master manages access requests' do background do project.request_access(user) - project.team << [master, :master] + project.add_master(master) sign_in(master) end diff --git a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb index 04806f8fd9e..47911c32a72 100644 --- a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb +++ b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb @@ -5,7 +5,7 @@ feature 'Projects > Members > Member cannot request access to his project' do let(:project) { create(:project) } background do - project.team << [member, :developer] + project.add_developer(member) sign_in(member) visit project_path(project) end diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 1bcf827d33c..e54c2c76975 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -5,7 +5,7 @@ feature 'Projects > Members > Member leaves project' do let(:project) { create(:project, :repository) } background do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit project_path(project) end diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 3198798306c..4cf48098401 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -122,7 +122,7 @@ feature 'Project > Members > Share with Group', :js do select2 group.id, from: '#link_group_id' fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') - page.find('body').click + click_on 'share-with-group-tab' find('.btn-create').click end diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb deleted file mode 100644 index a879efef4b5..00000000000 --- a/spec/features/projects/merge_requests/list_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -feature 'Merge Requests List' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - background do - project.team << [user, :developer] - - sign_in(user) - end - - scenario 'user does not see create new list button' do - create(:merge_request, source_project: project) - - visit project_merge_requests_path(project) - - expect(page).not_to have_selector('.js-new-board-list') - end - - it 'should show an empty state' do - visit project_merge_requests_path(project) - - expect(page).to have_selector('.empty-state') - end - - it 'empty state should have a create merge request button' do - visit project_merge_requests_path(project) - - expect(page).to have_link 'New merge request', href: project_new_merge_request_path(project) - end - - context 'if there are merge requests' do - before do - create(:merge_request, assignee: user, source_project: project) - - visit project_merge_requests_path(project) - end - - it 'should not show an empty state' do - expect(page).not_to have_selector('.empty-state') - end - end -end diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb index 4ca435491cb..f55eb5c6664 100644 --- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb +++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb @@ -13,20 +13,18 @@ describe 'User manages subscription', :js do end it 'toggles subscription' do - subscribe_button = find('.js-issuable-subscribe-button') + page.within('.js-issuable-subscribe-button') do + expect(page).to have_css 'button:not(.is-checked)' + find('button:not(.is-checked)').click - expect(subscribe_button).to have_content('Subscribe') + wait_for_requests - click_on('Subscribe') + expect(page).to have_css 'button.is-checked' + find('button.is-checked').click - wait_for_requests + wait_for_requests - expect(subscribe_button).to have_content('Unsubscribe') - - click_on('Unsubscribe') - - wait_for_requests - - expect(subscribe_button).to have_content('Subscribe') + expect(page).to have_css 'button:not(.is-checked)' + end end end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 013ed6f2e58..2e334caa98f 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -8,7 +8,7 @@ feature 'Pages' do background do allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - project.team << [user, role] + project.add_role(user, role) sign_in(user) end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index fa2f7a1fd78..65e24862d43 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -168,11 +168,11 @@ feature 'Pipeline Schedules', :js do scenario 'user sees the new variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123') - expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB') - expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123') + expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB') + expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123') end end end @@ -185,16 +185,18 @@ feature 'Pipeline Schedules', :js do visit_pipelines_schedules find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - all('[name="schedule[variables_attributes][][key]"]')[0].set('foo') - all('[name="schedule[variables_attributes][][value]"]')[0].set('bar') + + find('.js-ci-variable-list-section .js-secret-value-reveal-button').click + first('.js-ci-variable-input-key').set('foo') + first('.js-ci-variable-input-value').set('bar') click_button 'Save pipeline schedule' end scenario 'user sees the updated variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar') end end end @@ -207,15 +209,15 @@ feature 'Pipeline Schedules', :js do visit_pipelines_schedules find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - find('.pipeline-variable-list .pipeline-variable-row-remove-button').click + find('.ci-variable-list .ci-variable-row-remove-button').click click_button 'Save pipeline schedule' end scenario 'user does not see the removed variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('') end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3987cea0b4f..266ef693d0b 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -6,7 +6,7 @@ describe 'Pipeline', :js do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end shared_context 'pipeline builds' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index b87b47d0e1a..592c99fc64a 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -8,7 +8,7 @@ describe 'Pipelines', :js do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end describe 'GET /:project/pipelines' do @@ -545,6 +545,40 @@ describe 'Pipelines', :js do end end end + + describe 'Reset runner caches' do + let(:project) { create(:project, :repository) } + + before do + create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master') + project.add_master(user) + visit project_pipelines_path(project) + end + + it 'has a clear caches button' do + expect(page).to have_link 'Clear runner caches' + end + + describe 'user clicks the button' do + context 'when project already has jobs_cache_index' do + before do + project.update_attributes(jobs_cache_index: 1) + end + + it 'increments jobs_cache_index' do + click_link 'Clear runner caches' + expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' + end + end + + context 'when project does not have jobs_cache_index' do + it 'sets jobs_cache_index to 1' do + click_link 'Clear runner caches' + expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' + end + end + end + end end context 'when user is not logged in' do diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb index ac78b1dfb1c..028669eeaf2 100644 --- a/spec/features/projects/services/user_activates_jira_spec.rb +++ b/spec/features/projects/services/user_activates_jira_spec.rb @@ -18,7 +18,7 @@ describe 'User activates Jira', :js do end before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_settings_integrations_path(project) diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index 6f057137867..b2906e315f7 100644 --- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb @@ -8,7 +8,7 @@ feature 'Setup Mattermost slash commands', :js do before do stub_mattermost_setting(enabled: mattermost_enabled) - project.team << [user, :master] + project.add_master(user) sign_in(user) visit edit_project_service_path(project, service) end diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb index a8baf126269..4a88654210c 100644 --- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb @@ -6,7 +6,7 @@ feature 'Slack slash commands' do given(:service) { project.create_slack_slash_commands_service } background do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit edit_project_service_path(project, service) end diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index cbdb7973ac8..f6a1a46df11 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -8,7 +8,7 @@ feature 'Integration settings' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end context 'for developer' do diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index ac76c30cc7c..015db603d33 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -5,7 +5,7 @@ feature 'Project settings > Merge Requests', :js do let(:user) { create(:user) } background do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 561f08cba00..d0720855564 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -7,7 +7,7 @@ feature "Pipelines settings" do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end context 'for developer' do diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index e2a5619c22b..14670e91006 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -6,7 +6,7 @@ feature 'Repository settings' do let(:role) { :developer } background do - project.team << [user, role] + project.add_role(user, role) sign_in(user) end @@ -43,7 +43,7 @@ feature 'Repository settings' do fill_in 'deploy_key_title', with: 'new_deploy_key' fill_in 'deploy_key_key', with: new_ssh_key - check 'deploy_key_can_push' + check 'deploy_key_deploy_keys_projects_attributes_0_can_push' click_button 'Add key' expect(page).to have_content('new_deploy_key') @@ -57,7 +57,7 @@ feature 'Repository settings' do find('li', text: private_deploy_key.title).click_link('Edit') fill_in 'deploy_key_title', with: 'updated_deploy_key' - check 'deploy_key_can_push' + check 'deploy_key_deploy_keys_projects_attributes_0_can_push' click_button 'Save changes' expect(page).to have_content('updated_deploy_key') @@ -66,7 +66,7 @@ feature 'Repository settings' do scenario 'edit a deploy key from projects user has access to' do project2 = create(:project_empty_repo) - project2.team << [user, role] + project2.add_role(user, role) project2.deploy_keys << private_deploy_key visit project_settings_repository_path(project) @@ -74,11 +74,9 @@ feature 'Repository settings' do find('li', text: private_deploy_key.title).click_link('Edit') fill_in 'deploy_key_title', with: 'updated_deploy_key' - check 'deploy_key_can_push' click_button 'Save changes' expect(page).to have_content('updated_deploy_key') - expect(page).to have_content('Write access allowed') end scenario 'remove an existing deploy key' do diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 1c3b84d0114..06f6702670b 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -31,7 +31,7 @@ feature 'Visibility settings', :js do let(:master_user) { create(:user) } before do - project.team << [master_user, :master] + project.add_master(master_user) sign_in(master_user) visit edit_project_path(project) end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index e4215291f99..3466a3dfb77 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -16,7 +16,7 @@ feature 'Create Snippet', :js do context 'when a user is authenticated' do before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_snippets_path(project) diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb index 08dc7cf6c5b..216f2af7c88 100644 --- a/spec/features/projects/snippets/show_spec.rb +++ b/spec/features/projects/snippets/show_spec.rb @@ -6,7 +6,7 @@ feature 'Project snippet', :js do let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index d38a5b1324b..b62498194c4 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -24,7 +24,7 @@ feature 'Download buttons in tags page' do background do sign_in(user) - project.team << [user, role] + project.add_role(user, role) end describe 'when checking tags' do diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 3f6d16c8acf..0c67196f53e 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -14,7 +14,7 @@ feature 'Multi-file editor new directory', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index ba71eef07f4..85f7318c05d 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -14,7 +14,7 @@ feature 'Multi-file editor new file', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb index 4f2e0a76a65..6407370ac0d 100644 --- a/spec/features/projects/tree/rss_spec.rb +++ b/spec/features/projects/tree/rss_spec.rb @@ -7,7 +7,7 @@ feature 'Project Tree RSS' do context 'when signed in' do before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit path end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb index 9fbb1dbd0e8..f81e8677e92 100644 --- a/spec/features/projects/tree/upload_file_spec.rb +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -16,7 +16,7 @@ feature 'Multi-file editor upload file', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb index f5e4d7f5130..62e6419cc42 100644 --- a/spec/features/projects/user_browses_files_spec.rb +++ b/spec/features/projects/user_browses_files_spec.rb @@ -15,7 +15,7 @@ describe 'User browses files' do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb index 052cb3188c5..00e48f6fabd 100644 --- a/spec/features/projects/user_creates_directory_spec.rb +++ b/spec/features/projects/user_creates_directory_spec.rb @@ -11,7 +11,7 @@ feature 'User creates a directory', :js do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) sign_in(user) visit project_tree_path(project, 'master') end @@ -63,7 +63,7 @@ feature 'User creates a directory', :js do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb index d84b91ddc32..7a935dd2477 100644 --- a/spec/features/projects/user_creates_files_spec.rb +++ b/spec/features/projects/user_creates_files_spec.rb @@ -12,7 +12,7 @@ describe 'User creates files' do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -33,7 +33,7 @@ describe 'User creates files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end @@ -131,7 +131,7 @@ describe 'User creates files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb index 9e4e92ec076..9d55197e719 100644 --- a/spec/features/projects/user_deletes_files_spec.rb +++ b/spec/features/projects/user_deletes_files_spec.rb @@ -17,7 +17,7 @@ describe 'User deletes files' do context 'when an user has write access' do before do - project.team << [user, :master] + project.add_master(user) visit(project_tree_path_root_ref) end @@ -37,7 +37,7 @@ describe 'User deletes files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index d26ee653415..05c2be473da 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -14,7 +14,7 @@ describe 'User edits files' do context 'when an user has write access' do before do - project.team << [user, :master] + project.add_master(user) visit(project_tree_path_root_ref) end @@ -33,7 +33,9 @@ describe 'User edits files' do binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png') visit(project_blob_path(project, binary_file)) - expect(page).not_to have_link('edit') + page.within '.content' do + expect(page).not_to have_link('edit') + end end it 'commits an edited file', :js do @@ -87,7 +89,7 @@ describe 'User edits files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb index 245b6aa285b..74872403b35 100644 --- a/spec/features/projects/user_replaces_files_spec.rb +++ b/spec/features/projects/user_replaces_files_spec.rb @@ -19,7 +19,7 @@ describe 'User replaces files' do context 'when an user has write access' do before do - project.team << [user, :master] + project.add_master(user) visit(project_tree_path_root_ref) end @@ -45,7 +45,7 @@ describe 'User replaces files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb index ae51901adc6..75898afcda9 100644 --- a/spec/features/projects/user_uploads_files_spec.rb +++ b/spec/features/projects/user_uploads_files_spec.rb @@ -14,7 +14,7 @@ describe 'User uploads files' do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end @@ -50,7 +50,7 @@ describe 'User uploads files' do context 'when an user does not have write access' do before do - project2.team << [user, :reporter] + project2.add_reporter(user) visit(project2_tree_path_root_ref) end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 337baaf4dcd..006c15d60c5 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -13,7 +13,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do end background do - project.team << [user, :master] + project.add_master(user) sign_in(user) diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 949d90a50ff..4d2a08afecc 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -describe 'User updates wiki page' do +# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages +describe 'User updates wiki page', :skip_gitaly_mock do let(:user) { create(:user) } before do @@ -143,6 +144,7 @@ describe 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update home') fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') expect(page).to have_content('Home') @@ -151,4 +153,74 @@ describe 'User updates wiki page' do end end end + + context 'when the page is in a subdir' do + let!(:project) { create(:project, namespace: user.namespace) } + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let(:page_name) { 'page_name' } + let(:page_dir) { "foo/bar/#{page_name}" } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) } + + before do + visit(project_wiki_edit_path(project, wiki_page)) + end + + it 'moves the page to the root folder' do + fill_in(:wiki_title, with: "/#{page_name}") + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, page_name)) + end + + it 'moves the page to other dir' do + new_page_dir = "foo1/bar1/#{page_name}" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end + + it 'remains in the same place if title has not changed' do + original_path = project_wiki_path(project, wiki_page) + + fill_in(:wiki_title, with: page_name) + + click_button('Save changes') + + expect(current_path).to eq(original_path) + end + + it 'can be moved to a different dir with a different name' do + new_page_dir = "foo1/bar1/new_page_name" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end + + it 'can be renamed and moved to the root folder' do + new_name = 'new_page_name' + + fill_in(:wiki_title, with: "/#{new_name}") + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_name)) + end + + it 'squishes the title before creating the page' do + new_page_dir = " foo1 / bar1 / #{page_name} " + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) + end + end end diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index ebb3bd044c1..2682b62fa04 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -4,7 +4,7 @@ describe 'Projects > Wiki > User views wiki in project page' do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index ff325aeadd3..e37436838fd 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -describe 'User views a wiki page' do +# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages +describe 'User views a wiki page', :skip_gitaly_mock do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } let(:wiki_page) do diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 63e6051b571..b66a7dea598 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -133,7 +133,7 @@ feature 'Project' do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) visit edit_project_path(project) end @@ -151,7 +151,7 @@ feature 'Project' do let(:project) { create(:forked_project_with_submodules) } before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_path(project) end @@ -180,7 +180,7 @@ feature 'Project' do let(:project) { create(:project, :repository) } before do - project.team << [user, :master] + project.add_master(user) sign_in user visit project_path(project) end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index c7f0e342809..aec9de6c7ca 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -33,6 +33,26 @@ feature 'Runners' do expect(page).to have_content(specific_runner.platform) end + scenario 'user can pause and resume the specific runner' do + visit runners_path(project) + + within '.activated-specific-runners' do + expect(page).to have_content('Pause') + end + + click_on 'Pause' + + within '.activated-specific-runners' do + expect(page).to have_content('Resume') + end + + click_on 'Resume' + + within '.activated-specific-runners' do + expect(page).to have_content('Pause') + end + end + scenario 'user removes an activated specific runner if this is last project for that runners' do visit runners_path(project) diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 8efa5b58141..6088b831c14 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -5,7 +5,7 @@ describe 'GPG signed commits', :js do it 'changes from unverified to verified when the user changes his email to match the gpg key' do user = create :user, email: 'unrelated.user@example.org' - project.team << [user, :master] + project.add_master(user) Sidekiq::Testing.inline! do create :gpg_key, key: GpgHelpers::User1.public_key, user: user @@ -36,7 +36,7 @@ describe 'GPG signed commits', :js do it 'changes from unverified to verified when the user adds the missing gpg key' do user = create :user, email: GpgHelpers::User1.emails.first - project.team << [user, :master] + project.add_master(user) sign_in(user) @@ -86,7 +86,7 @@ describe 'GPG signed commits', :js do before do user = create :user - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index 1f8bd8d681e..8a8f6933fa5 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -5,7 +5,7 @@ feature 'Master creates tag' do let(:project) { create(:project, :repository, namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index dfda664d673..c0b4fa52526 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -5,7 +5,7 @@ feature 'Master deletes tag' do let(:project) { create(:project, :repository, namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_tags_path(project) end diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb index b93ad44dfd3..1c370a99b13 100644 --- a/spec/features/tags/master_updates_tag_spec.rb +++ b/spec/features/tags/master_updates_tag_spec.rb @@ -5,7 +5,7 @@ feature 'Master updates tag' do let(:project) { create(:project, :repository, namespace: user.namespace) } before do - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_tags_path(project) end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index bc472e74997..19784120108 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -10,9 +10,9 @@ feature 'Triggers', :js do sign_in(user) @project = create(:project) - @project.team << [user, :master] - @project.team << [user2, :master] - @project.team << [guest_user, :guest] + @project.add_master(user) + @project.add_master(user2) + @project.add_guest(guest_user) visit project_settings_ci_cd_path(@project) end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index c9afef2a8de..50ee1656e10 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -264,7 +264,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do end it "deletes u2f registrations" do - visit profile_account_path + visit profile_two_factor_auth_path expect do accept_confirm { click_on "Disable" } end.to change { U2fRegistration.count }.by(-1) diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 670e8dda916..975c157bcf5 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe 'User can display performance bar', :js do - shared_examples 'performance bar is disabled' do + shared_examples 'performance bar cannot be displayed' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') end @@ -17,7 +17,7 @@ describe 'User can display performance bar', :js do end end - shared_examples 'performance bar is enabled' do + shared_examples 'performance bar can be displayed' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') end @@ -33,6 +33,18 @@ describe 'User can display performance bar', :js do end end + shared_examples 'performance bar is enabled by default in development' do + before do + allow(Rails.env).to receive(:development?).and_return(true) + end + + it 'shows the performance bar by default' do + refresh # Because we're stubbing Rails.env after the 1st visit to root_path + + expect(page).to have_css('#peek') + end + end + let(:group) { create(:group) } context 'when user is logged-out' do @@ -45,7 +57,7 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: nil) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' end context 'when the performance_bar feature is enabled' do @@ -53,7 +65,7 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: group.id) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' end end @@ -72,7 +84,8 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: nil) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' + it_behaves_like 'performance bar is enabled by default in development' end context 'when the performance_bar feature is enabled' do @@ -80,7 +93,8 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: group.id) end - it_behaves_like 'performance bar is enabled' + it_behaves_like 'performance bar is enabled by default in development' + it_behaves_like 'performance bar can be displayed' end end end diff --git a/spec/features/user_page_spec.rb b/spec/features/user_page_spec.rb new file mode 100644 index 00000000000..19c587e53c8 --- /dev/null +++ b/spec/features/user_page_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe 'User page', :js do + let!(:user) { create :user } + let!(:private_project) do + create :project, :private, name: 'private', namespace: user.namespace do |project| + project.add_master(user) + end + end + + let!(:internal_project) do + create :project, :internal, name: 'internal', namespace: user.namespace do |project| + project.add_master(user) + end + end + + let!(:public_project) do + create :project, :public, name: 'public', namespace: user.namespace do |project| + project.add_master(user) + end + end + + def click_nav_link(name) + page.within '.nav-links' do + click_link name + end + end + + context 'when not signed in' do + it 'renders user public project' do + visit user_path(user) + click_nav_link('Personal projects') + + expect(page).to have_css('.tab-content #projects.active') + expect(title).to start_with(user.name) + + expect(page).to have_content(public_project.name) + expect(page).not_to have_content(private_project.name) + expect(page).not_to have_content(internal_project.name) + end + end + + context 'when signed in as another user' do + let(:another_user) { create :user } + + before do + sign_in(another_user) + end + + it 'renders user public and internal projects' do + visit user_path(user) + click_nav_link('Personal projects') + + expect(title).to start_with(user.name) + + expect(page).not_to have_content(private_project.name) + expect(page).to have_content(public_project.name) + expect(page).to have_content(internal_project.name) + end + end + + context 'when signed in as user' do + before do + sign_in(user) + end + + describe 'personal projects' do + it 'renders all user projects' do + visit user_path(user) + click_nav_link('Personal projects') + + expect(title).to start_with(user.name) + + expect(page).to have_content(private_project.name) + expect(page).to have_content(public_project.name) + expect(page).to have_content(internal_project.name) + end + end + + describe 'contributed projects' do + context 'when user has contributions' do + let(:contributed_project) do + create :project, :public, :empty_repo + end + + before do + Issues::CreateService.new(contributed_project, user, { title: 'Bug in old browser' }).execute + event = create(:push_event, project: contributed_project, author: user) + create(:push_event_payload, event: event, commit_count: 3) + end + + it 'renders contributed project' do + visit user_path(user) + + expect(title).to start_with(user.name) + expect(page).to have_css('.js-contrib-calendar') + + click_nav_link('Contributed projects') + + page.within '#contributed' do + expect(page).to have_content(contributed_project.name) + end + end + end + end + end +end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index dde60c83536..79ca2b4bb4a 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -7,7 +7,7 @@ describe 'Project variables', :js do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) project.variables << variable visit project_settings_ci_cd_path(project) diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb index 0789d3a9b44..650f7229647 100644 --- a/spec/finders/access_requests_finder_spec.rb +++ b/spec/finders/access_requests_finder_spec.rb @@ -51,7 +51,7 @@ describe AccessRequestsFinder do context 'when current user can see access requests' do before do - project.team << [user, :master] + project.add_master(user) group.add_owner(user) end @@ -78,7 +78,7 @@ describe AccessRequestsFinder do context 'when current user can see access requests' do before do - project.team << [user, :master] + project.add_master(user) group.add_owner(user) end diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index ae050f36b4a..375bcc9087e 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -35,6 +35,15 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(project) end + it 'does not include projects shared with the group' do + project = create(:project, namespace: group) + other_project = create(:project) + other_project.project_group_links.create(group: group, + group_access: ProjectGroupLink::MASTER) + + expect(finder.execute).to contain_exactly(project) + end + context 'when archived is `true`' do let(:params) { { archived: 'true' } } @@ -189,6 +198,17 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(subgroup, matching_project) end + context 'with a small page size' do + let(:params) { { filter: 'test', per_page: 1 } } + + it 'contains all the ancestors of a matching subgroup regardless the page size' do + subgroup = create(:group, :private, parent: group) + matching = create(:group, :private, name: 'testgroup', parent: subgroup) + + expect(finder.execute).to contain_exactly(subgroup, matching) + end + end + it 'does not include the parent itself' do group.update!(name: 'test') diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index c6d257bc479..be80ee7d767 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe GroupProjectsFinder do let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } let(:current_user) { create(:user) } let(:options) { {} } @@ -12,6 +13,8 @@ describe GroupProjectsFinder do let!(:shared_project_1) { create(:project, :public, path: '3') } let!(:shared_project_2) { create(:project, :private, path: '4') } let!(:shared_project_3) { create(:project, :internal, path: '5') } + let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) } + let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } before do shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group) @@ -35,17 +38,37 @@ describe GroupProjectsFinder do context "only owned" do let(:options) { { only_owned: true } } - it { is_expected.to match_array([private_project, public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to match_array([private_project, public_project]) } + end end context "all" do - it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project, subgroup_project, subgroup_private_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + end end end describe 'without group member current_user' do before do - shared_project_2.team << [current_user, Gitlab::Access::MASTER] + shared_project_2.add_master(current_user) current_user.reload end @@ -70,10 +93,21 @@ describe GroupProjectsFinder do context "without external user" do before do - private_project.team << [current_user, Gitlab::Access::MASTER] + private_project.add_master(current_user) + subgroup_private_project.add_master(current_user) end - it { is_expected.to match_array([private_project, public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to match_array([private_project, public_project]) } + end end context "with external user" do @@ -81,12 +115,32 @@ describe GroupProjectsFinder do current_user.update_attributes(external: true) end - it { is_expected.to eq([public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([public_project, subgroup_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to eq([public_project]) } + end end end context "all" do - it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project, subgroup_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) } + end end end @@ -100,7 +154,17 @@ describe GroupProjectsFinder do context "only owned" do let(:options) { { only_owned: true } } - it { is_expected.to eq([public_project]) } + context 'with subgroups projects', :nested_groups do + before do + options[:include_subgroups] = true + end + + it { is_expected.to match_array([public_project, subgroup_project]) } + end + + context 'without subgroups projects' do + it { is_expected.to eq([public_project]) } + end end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 47b173dea0a..abb7631d7d7 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -3,13 +3,17 @@ require 'spec_helper' describe IssuesFinder do set(:user) { create(:user) } set(:user2) { create(:user) } - set(:project1) { create(:project) } + set(:group) { create(:group) } + set(:subgroup) { create(:group, parent: group) } + set(:project1) { create(:project, group: group) } set(:project2) { create(:project) } + set(:project3) { create(:project, group: subgroup) } set(:milestone) { create(:milestone, project: project1) } set(:label) { create(:label, project: project2) } set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) } set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') } set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) } + set(:issue4) { create(:issue, project: project3) } set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } @@ -22,13 +26,15 @@ describe IssuesFinder do let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } before(:context) do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] + project1.add_master(user) + project2.add_developer(user) + project2.add_developer(user2) + project3.add_developer(user) issue1 issue2 issue3 + issue4 award_emoji1 award_emoji2 @@ -39,7 +45,7 @@ describe IssuesFinder do let(:scope) { 'all' } it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3) + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) end context 'filtering by assignee ID' do @@ -50,6 +56,26 @@ describe IssuesFinder do end end + context 'filtering by group_id' do + let(:params) { { group_id: group.id } } + + context 'when include_subgroup param not set' do + it 'returns all group issues' do + expect(issues).to contain_exactly(issue1) + end + end + + context 'when include_subgroup param is true', :nested_groups do + before do + params[:include_subgroups] = true + end + + it 'returns all group and subgroup issues' do + expect(issues).to contain_exactly(issue1, issue4) + end + end + end + context 'filtering by author ID' do let(:params) { { author_id: user2.id } } @@ -87,7 +113,7 @@ describe IssuesFinder do let(:params) { { milestone_title: Milestone::None.title } } it 'returns issues with no milestone' do - expect(issues).to contain_exactly(issue2, issue3) + expect(issues).to contain_exactly(issue2, issue3, issue4) end end @@ -185,7 +211,7 @@ describe IssuesFinder do let(:params) { { label_name: Label::None.title } } it 'returns issues with no labels' do - expect(issues).to contain_exactly(issue1, issue3) + expect(issues).to contain_exactly(issue1, issue3, issue4) end end @@ -210,7 +236,7 @@ describe IssuesFinder do let(:params) { { state: 'opened' } } it 'returns only opened issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3) + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) end end @@ -226,7 +252,7 @@ describe IssuesFinder do let(:params) { { state: 'all' } } it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4) end end @@ -234,7 +260,7 @@ describe IssuesFinder do let(:params) { { state: 'invalid_state' } } it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4) end end end @@ -338,7 +364,7 @@ describe IssuesFinder do end it "doesn't return issues if feature disabled" do - [project1, project2].each do |project| + [project1, project2, project3].each do |project| project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) end @@ -351,7 +377,7 @@ describe IssuesFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(3) + expect(finder.row_count).to eq(4) end it 'returns the number of rows for a given state' do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index afa2a40ed2a..06031aee217 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -27,7 +27,7 @@ describe LabelsFinder do create(:label, project: project_3, title: 'Label 3') create(:group_label, group: group_3, title: 'Group Label 4') - project_1.team << [user, :developer] + project_1.add_developer(user) end context 'with no filter' do @@ -56,6 +56,16 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end + + context 'when only_group_labels is true' do + it 'returns only group labels' do + group_1.add_developer(user) + + finder = described_class.new(user, group_id: group_1.id, only_group_labels: true) + + expect(finder.execute).to eq [group_label_2, group_label_1] + end + end end context 'filtering by project_id' do @@ -73,7 +83,7 @@ describe LabelsFinder do # project_3 has a label associated to it, which we don't want coming # back when we ask for the isolated project's labels - project_3.team << [admin, :reporter] + project_3.add_reporter(admin) finder = described_class.new(admin, project_id: isolated_project.id) expect(finder.execute).to be_empty diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 883bdf3746a..9385c892c9e 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -6,31 +6,36 @@ describe MergeRequestsFinder do let(:user) { create :user } let(:user2) { create :user } - let(:project1) { create(:project, :public) } + let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } + let(:project1) { create(:project, :public, group: group) } let(:project2) { fork_project(project1, user) } let(:project3) do p = fork_project(project1, user) p.update!(archived: true) p end + let(:project4) { create(:project, :public, group: subgroup) } let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) } let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) } + let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) } before do - project1.team << [user, :master] - project2.team << [user, :developer] - project3.team << [user, :developer] - project2.team << [user2, :developer] + project1.add_master(user) + project2.add_developer(user) + project3.add_developer(user) + project2.add_developer(user2) + project4.add_developer(user) end describe "#execute" do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) + expect(merge_requests.size).to eq(4) end it 'filters by project' do @@ -39,10 +44,26 @@ describe MergeRequestsFinder do expect(merge_requests.size).to eq(1) end + it 'filters by group' do + params = { group_id: group.id } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests.size).to eq(2) + end + + it 'filters by group including subgroups', :nested_groups do + params = { group_id: group.id, include_subgroups: true } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests.size).to eq(3) + end + it 'filters by non_archived' do params = { non_archived: true } merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) + expect(merge_requests.size).to eq(4) end it 'filters by iid' do @@ -73,14 +94,14 @@ describe MergeRequestsFinder do end context 'with created_after and created_before params' do - let(:project4) { create(:project, forked_from_project: project1) } + let(:new_project) { create(:project, forked_from_project: project1) } let!(:new_merge_request) do create(:merge_request, :simple, author: user, created_at: 1.week.from_now, - source_project: project4, + source_project: new_project, target_project: project1) end @@ -89,12 +110,12 @@ describe MergeRequestsFinder do :simple, author: user, created_at: 1.week.ago, - source_project: project4, - target_project: project4) + source_project: new_project, + target_project: new_project) end before do - project4.add_master(user) + new_project.add_master(user) end it 'filters by created_after' do @@ -106,7 +127,7 @@ describe MergeRequestsFinder do end it 'filters by created_before' do - params = { project_id: project4.id, created_before: old_merge_request.created_at + 1.second } + params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second } merge_requests = described_class.new(user, params).execute @@ -119,7 +140,7 @@ describe MergeRequestsFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(3) + expect(finder.row_count).to eq(4) end it 'returns the number of rows for a given state' do diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 8ae08656e01..0b3cf7ece5f 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -21,10 +21,19 @@ describe MilestonesFinder do expect(result).to contain_exactly(milestone_1, milestone_2) end - it 'returns milestones for groups and projects' do - result = described_class.new(project_ids: [project_1.id, project_2.id], group_ids: group.id, state: 'all').execute + context 'milestones for groups and project' do + let(:result) do + described_class.new(project_ids: [project_1.id, project_2.id], group_ids: group.id, state: 'all').execute + end + + it 'returns milestones for groups and projects' do + expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3, milestone_4) + end - expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3, milestone_4) + it 'orders milestones by due date' do + expect(result.first).to eq(milestone_1) + expect(result.second).to eq(milestone_3) + end end context 'with filters' do @@ -61,30 +70,4 @@ describe MilestonesFinder do expect(result.to_a).to contain_exactly(milestone_1) end end - - context 'with order' do - let(:params) do - { - project_ids: [project_1.id, project_2.id], - group_ids: group.id, - state: 'all' - } - end - - it "default orders by due date" do - result = described_class.new(params).execute - - expect(result.first).to eq(milestone_1) - expect(result.second).to eq(milestone_3) - end - - it "orders by parameter" do - result = described_class.new(params.merge(order: 'id DESC')).execute - - expect(result.first).to eq(milestone_4) - expect(result.second).to eq(milestone_3) - expect(result.third).to eq(milestone_2) - expect(result.fourth).to eq(milestone_1) - end - end end diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb index e577083a2d0..74639d4147f 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/move_to_project_finder_spec.rb @@ -15,39 +15,39 @@ describe MoveToProjectFinder do describe '#execute' do context 'filter' do it 'does not return projects under Gitlab::Access::REPORTER' do - guest_project.team << [user, :guest] + guest_project.add_guest(user) expect(subject.execute(project)).to be_empty end it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do - reporter_project.team << [user, :reporter] - developer_project.team << [user, :developer] - master_project.team << [user, :master] + reporter_project.add_reporter(user) + developer_project.add_developer(user) + master_project.add_master(user) expect(subject.execute(project).to_a).to eq([master_project, developer_project, reporter_project]) end it 'does not include the source project' do - project.team << [user, :reporter] + project.add_reporter(user) expect(subject.execute(project).to_a).to be_empty end it 'does not return archived projects' do - reporter_project.team << [user, :reporter] + reporter_project.add_reporter(user) reporter_project.archive! other_reporter_project = create(:project) - other_reporter_project.team << [user, :reporter] + other_reporter_project.add_reporter(user) expect(subject.execute(project).to_a).to eq([other_reporter_project]) end it 'does not return projects for which issues are disabled' do - reporter_project.team << [user, :reporter] + reporter_project.add_reporter(user) reporter_project.update_attributes(issues_enabled: false) other_reporter_project = create(:project) - other_reporter_project.team << [user, :reporter] + other_reporter_project.add_reporter(user) expect(subject.execute(project).to_a).to eq([other_reporter_project]) end @@ -55,9 +55,9 @@ describe MoveToProjectFinder do it 'returns a page of projects ordered by id in descending order' do stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 - reporter_project.team << [user, :reporter] - developer_project.team << [user, :developer] - master_project.team << [user, :master] + reporter_project.add_reporter(user) + developer_project.add_developer(user) + master_project.add_master(user) expect(subject.execute(project).to_a).to eq([master_project, developer_project]) end @@ -65,9 +65,9 @@ describe MoveToProjectFinder do it 'returns projects after the given offset id' do stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 - reporter_project.team << [user, :reporter] - developer_project.team << [user, :developer] - master_project.team << [user, :master] + reporter_project.add_reporter(user) + developer_project.add_developer(user) + master_project.add_master(user) expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project]) expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) @@ -84,10 +84,10 @@ describe MoveToProjectFinder do it 'returns projects matching a search query' do foo_project = create(:project) - foo_project.team << [user, :master] + foo_project.add_master(user) wadus_project = create(:project, name: 'wadus') - wadus_project.team << [user, :master] + wadus_project.add_master(user) expect(subject.execute(project).to_a).to eq([wadus_project, foo_project]) expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project]) diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 900fa2b12d1..7b43494eea2 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -5,7 +5,7 @@ describe NotesFinder do let(:project) { create(:project) } before do - project.team << [user, :master] + project.add_master(user) end describe '#execute' do @@ -147,7 +147,7 @@ describe NotesFinder do it 'raises an error for project members with guest role' do user = create(:user) - project.team << [user, :guest] + project.add_guest(user) expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) end @@ -189,7 +189,7 @@ describe NotesFinder do it "does not return notes with matching content for project members with guest role" do user = create(:user) - project.team << [user, :guest] + project.add_guest(user) expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb index d0113ba87df..5e52898e9c0 100644 --- a/spec/finders/personal_projects_finder_spec.rb +++ b/spec/finders/personal_projects_finder_spec.rb @@ -15,7 +15,7 @@ describe PersonalProjectsFinder do end before do - private_project.team << [current_user, Gitlab::Access::DEVELOPER] + private_project.add_developer(current_user) end describe 'without a current user' do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 7ae7b7d2140..0a018d2b417 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -188,7 +188,7 @@ describe SnippetsFinder do end it "returns all snippets for project members" do - project1.team << [user, :developer] + project1.add_developer(user) snippets = described_class.new(user, project: project1).execute @@ -196,7 +196,7 @@ describe SnippetsFinder do end it "returns private snippets for project members" do - project1.team << [user, :developer] + project1.add_developer(user) snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index 884ce22091e..90eb0fe21e4 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -7,7 +7,7 @@ describe TodosFinder do let(:finder) { described_class } before do - project.team << [user, :developer] + project.add_developer(user) end describe '#sort' do diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json index 3d3329a3406..38467b4ca20 100644 --- a/spec/fixtures/api/schemas/entities/issue.json +++ b/spec/fixtures/api/schemas/entities/issue.json @@ -28,7 +28,6 @@ "confidential": { "type": "boolean" }, "discussion_locked": { "type": ["boolean", "null"] }, "updated_by_id": { "type": ["string", "null"] }, - "deleted_at": { "type": ["string", "null"] }, "time_estimate": { "type": "integer" }, "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 995f13381ad..f1199468d53 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -9,6 +9,7 @@ "human_time_estimate": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, "assignee_id": { "type": ["integer", "null"] }, "subscribed": { "type": ["boolean", "null"] }, "participants": { "type": "array" } diff --git a/spec/fixtures/api/schemas/entities/merge_request_metrics.json b/spec/fixtures/api/schemas/entities/merge_request_metrics.json new file mode 100644 index 00000000000..3fa767f85df --- /dev/null +++ b/spec/fixtures/api/schemas/entities/merge_request_metrics.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required": ["closed_at", "merged_at", "closed_by", "merged_by"], + "properties" : { + "closed_at": { "type": ["datetime", "null"] }, + "merged_at": { "type": ["datetime", "null"] }, + "closed_by": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + }, + "merged_by": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 342890c3dee..05461787f06 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -13,7 +13,6 @@ "updated_by_id": { "type": ["string", "null"] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, - "deleted_at": { "type": ["string", "null"] }, "time_estimate": { "type": "integer" }, "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, @@ -31,8 +30,12 @@ "source_project_id": { "type": "integer" }, "target_branch": { "type": "string" }, "target_project_id": { "type": "integer" }, - "merge_event": { "type": ["object", "null"] }, - "closed_event": { "type": ["object", "null"] }, + "metrics": { + "oneOf": [ + { "type": "null" }, + { "$ref": "merge_request_metrics.json" } + ] + }, "author": { "type": ["object", "null"] }, "merge_user": { "type": ["object", "null"] }, "diff_head_sha": { "type": ["string", "null"] }, @@ -99,7 +102,11 @@ "remove_source_branch": { "type": ["boolean", "null"] }, "merge_ongoing": { "type": "boolean" }, "ff_only_enabled": { "type": ["boolean", false] }, - "should_be_rebased": { "type": "boolean" } + "should_be_rebased": { "type": "boolean" }, + "rebase_commit_sha": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, + "can_push_to_source_branch": { "type": "boolean" }, + "rebase_path": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json new file mode 100644 index 00000000000..6482e0eedd2 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/user.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "required": [ + "id", + "state", + "avatar_url", + "web_url", + "path" + ], + "properties": { + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "string" }, + "web_url": { "type": "string" }, + "path": { "type": "string" } + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json new file mode 100644 index 00000000000..d667f1d631c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "required" : [ + "id", + "project", + "lists" + ], + "properties" : { + "id": { "type": "integer" }, + "project": { + "type": ["object", "null"], + "required": [ + "id", + "avatar_url", + "description", + "default_branch", + "tag_list", + "ssh_url_to_repo", + "http_url_to_repo", + "web_url", + "name", + "name_with_namespace", + "path", + "path_with_namespace", + "star_count", + "forks_count", + "created_at", + "last_activity_at" + ], + "properties": { + "id": { "type": "integer" }, + "avatar_url": { "type": ["string", "null"] }, + "description": { "type": ["string", "null"] }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { "type": "array" }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "created_at": { "type": "date" }, + "last_activity_at": { "type": "date" } + }, + "additionalProperties": false + }, + "lists": { + "type": "array", + "items": { + "type": "object", + "required" : [ + "id", + "label", + "position" + ], + "properties" : { + "id": { "type": "integer" }, + "label": { + "type": ["object", "null"], + "required": [ + "id", + "color", + "description", + "name" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "name": { "type": "string" } + } + }, + "position": { "type": ["integer", "null"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json new file mode 100644 index 00000000000..117564ef77a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/boards.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "board.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json index 4ba6422406c..e8c17298b43 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -3,6 +3,7 @@ "properties": { "domain": { "type": "string" }, "url": { "type": "uri" }, + "project_id": { "type": "integer" }, "certificate_expiration": { "type": "object", "properties": { @@ -13,6 +14,6 @@ "additionalProperties": false } }, - "required": ["domain", "url"], + "required": ["domain", "url", "project_id"], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/pipelines.json b/spec/fixtures/api/schemas/public_api/v4/pipelines.json new file mode 100644 index 00000000000..8b08a00f708 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pipelines.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "pipeline/basic.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json index 9f69d31971c..2d815be32a6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json @@ -1,13 +1,17 @@ { - "type": "object", + "type": ["object", "null"], "required": [ "id", + "name", + "username", "state", "avatar_url", "web_url" ], "properties": { "id": { "type": "integer" }, + "name": { "type": "string" }, + "username": { "type": "string" }, "state": { "type": "string" }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" } diff --git a/spec/fixtures/emails/attachment.eml b/spec/fixtures/emails/attachment.eml index f25c3d1a449..b3a30b3221b 100644 --- a/spec/fixtures/emails/attachment.eml +++ b/spec/fixtures/emails/attachment.eml @@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= + <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= 532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= ax-width:694px" width=3D"45" height=3D"45"> </td> @@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p> <div style=3D"color:#666"> <p>To respond, reply to this email or visit <a href=3D"http://meta.disc= ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co= -lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= +lor:#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back= -up-in-suggested-topics/11005/5</a> in your browser.</p> </div> @@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= lpadding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= + <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= 532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= ax-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size= + <a href=3D"https://meta.discourse.org/users/neil" style=3D"font-size= :13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;c= olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<= /a><br> @@ -155,12 +155,12 @@ vember 19</span> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= + <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= 72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= "max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si= ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ing</a><br> @@ -173,7 +173,7 @@ vember 19</span> <td style=3D"padding-top:5px" colspan=3D"2"> <p style=3D"margin-top:0"><u></u></p><div> <div></div> -<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62= +<img width=3D"20" height=3D"20" src=3D"https://www.gravatar.com/avatar/51d62= 3f33f8b83095db84ff35e15dbe8.png?s=3D40&r=3Dpg&d=3Didenticon" style= =3D"max-width:694px">codinghorror:</div> <blockquote><p style=3D"margin-top:0">I can't even find that topic by n= @@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= + <img src=3D"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= 5e15dbe8.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"codinghorror" st= yle=3D"max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f= + <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"f= ont-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans= -serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan= k">codinghorror</a><br> @@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can't even find that topic by= adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= + <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= 72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= "max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si= ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ing</a><br> @@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in= <p style=3D"margin-top:0"></p> <div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c= -b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m= +b249e.png" target=3D"_blank"><img src=3D"https://cdn.discourse.org/uploads/m= eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig= ht=3D"134" style=3D"max-width:694px"><div> @@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div> <div style=3D"color:#666"> <p>To respond, reply to this email or visit <a href=3D"http://meta.discours= e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:= -#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-= +#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back-up-= in-suggested-topics/11005/5</a> in your browser.</p> </div> <div style=3D"color:#666"> -<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc= +<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.disc= ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre= ferences</a>.</p> </div> diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 71abb6da607..da32a46675f 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such: ![My Video](/assets/videos/gitlab-demo.mp4) +### Colors + +`#F00` +`#F00A` +`#FF0000` +`#FF0000AA` +`RGB(0,255,0)` +`RGB(0%,100%,0%)` +`RGBA(0,255,0,0.7)` +`HSL(540,70%,50%)` +`HSLA(540,70%,50%,0.7)` + ### Mermaid > If this is not rendered correctly, see diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5c5d53877a6..f7a4a7afced 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -100,7 +100,7 @@ describe ApplicationHelper do end it 'returns a generic avatar' do - expect(helper.gravatar_icon(user_email)).to match('no_avatar.png') + expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png') end end @@ -110,14 +110,14 @@ describe ApplicationHelper do end it 'returns a generic avatar when email is blank' do - expect(helper.gravatar_icon('')).to match('no_avatar.png') + expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png') end it 'returns a valid Gravatar URL' do stub_config_setting(https: false) expect(helper.gravatar_icon(user_email)) - .to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') + .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') end it 'uses HTTPs when configured' do diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 04620f6d88c..a030796c54e 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -22,6 +22,13 @@ describe BlobHelper do expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</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>] diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index f9c31ac61d8..15cbe36ae76 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -266,4 +266,14 @@ describe DiffHelper do end end end + + context '#diff_file_path_text' do + it 'returns full path by default' do + expect(diff_file_path_text(diff_file)).to eq(diff_file.new_path) + end + + it 'returns truncated path' do + expect(diff_file_path_text(diff_file, max: 10)).to eq("...open.rb") + end + end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 32432ee1e81..5f608fe18d9 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -105,7 +105,7 @@ describe GroupsHelper do it 'outputs the groups in the correct order' do expect(helper.group_title(very_deep_nested_group)) - .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m) + .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m) end end @@ -120,7 +120,7 @@ describe GroupsHelper do let(:possible_help_texts) do { default_help: "This setting will be applied to all subgroups unless overridden by a group owner", - ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/, + ancestor_locked_but_you_can_override: %r{This setting is applied on <a .+>.+</a>\. You can override the setting or .+}, ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/, ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/ } diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index d601cbdb39b..7fa665aecdc 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -125,10 +125,10 @@ describe IssuablesHelper do describe '#updated_at_by' do let(:user) { create(:user) } let(:unedited_issuable) { create(:issue) } - let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 2.days.ago, last_edited_at: 2.days.ago) } + let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 1.day.ago, last_edited_at: 2.days.ago) } let(:edited_updated_at_by) do { - updatedAt: edited_issuable.updated_at.to_time.iso8601, + updatedAt: edited_issuable.last_edited_at.to_time.iso8601, updatedBy: { name: user.name, path: user_path(user) @@ -142,7 +142,7 @@ describe IssuablesHelper do context 'when updated by a deleted user' do let(:edited_updated_at_by) do { - updatedAt: edited_issuable.updated_at.to_time.iso8601, + updatedAt: edited_issuable.last_edited_at.to_time.iso8601, updatedBy: { name: User.ghost.name, path: user_path(User.ghost) @@ -192,4 +192,33 @@ describe IssuablesHelper do expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data) end end + + describe '#selected_labels' do + context 'if label_name param is a string' do + it 'returns a new label with title' do + allow(helper).to receive(:params) + .and_return(ActionController::Parameters.new(label_name: 'test label')) + + labels = helper.selected_labels + + expect(labels).to be_an(Array) + expect(labels.size).to eq(1) + expect(labels.first.title).to eq('test label') + end + end + + context 'if label_name param is an array' do + it 'returns a new label with title for each element' do + allow(helper).to receive(:params) + .and_return(ActionController::Parameters.new(label_name: ['test label 1', 'test label 2'])) + + labels = helper.selected_labels + + expect(labels).to be_an(Array) + expect(labels.size).to eq(2) + expect(labels.first.title).to eq('test label 1') + expect(labels.second.title).to eq('test label 2') + end + end + end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0286d36952c..619baa78bfa 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -104,7 +104,7 @@ describe LabelsHelper do context 'with a tooltip argument' do context 'set to false' do it 'does not include the has-tooltip class' do - expect(link_to_label(label, tooltip: false)).not_to match %r{has-tooltip} + expect(link_to_label(label, tooltip: false)).not_to match /has-tooltip/ end end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index ba0039f3a11..c0dc9293397 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -11,7 +11,7 @@ describe MarkupHelper do before do # Ensure the generated reference links aren't redacted - project.team << [user, :master] + project.add_master(user) # Helper expects a @project instance variable helper.instance_variable_set(:@project, project) diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 36a44f8567a..b992bdb4a5e 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -17,9 +17,9 @@ describe NotesHelper do before do group.add_owner(owner) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) end describe "#notes_max_access_for_users" do @@ -31,7 +31,7 @@ describe NotesHelper do it 'handles access in different projects' do second_project = create(:project) - second_project.team << [master, :reporter] + second_project.add_reporter(master) other_note = create(:note, author: master, project: second_project) expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index ede9d232efd..c0251bf7dc0 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ProjectsHelper do + include ProjectForksHelper + describe "#project_status_css_class" do it "returns appropriate class" do expect(project_status_css_class("started")).to eq("active") @@ -10,9 +12,9 @@ describe ProjectsHelper do end describe "can_change_visibility_level?" do - let(:project) { create(:project, :repository) } + let(:project) { create(:project) } let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user } - let(:fork_project) { Projects::ForkService.new(project, user).execute } + let(:forked_project) { fork_project(project, user) } it "returns false if there are no appropriate permissions" do allow(helper).to receive(:can?) { false } @@ -26,21 +28,29 @@ describe ProjectsHelper do expect(helper.can_change_visibility_level?(project, user)).to be_truthy end + it 'allows visibility level to be changed if the project is forked' do + allow(helper).to receive(:can?).with(user, :change_visibility_level, project) { true } + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + fork_project(project) + + expect(helper.can_change_visibility_level?(project, user)).to be_truthy + end + context "forks" do it "returns false if there are permissions and origin project is PRIVATE" do allow(helper).to receive(:can?) { true } - project.update visibility_level: Gitlab::VisibilityLevel::PRIVATE + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - expect(helper.can_change_visibility_level?(fork_project, user)).to be_falsey + expect(helper.can_change_visibility_level?(forked_project, user)).to be_falsey end it "returns true if there are permissions and origin project is INTERNAL" do allow(helper).to receive(:can?) { true } - project.update visibility_level: Gitlab::VisibilityLevel::INTERNAL + project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - expect(helper.can_change_visibility_level?(fork_project, user)).to be_truthy + expect(helper.can_change_visibility_level?(forked_project, user)).to be_truthy end end end diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb new file mode 100644 index 00000000000..27455705d23 --- /dev/null +++ b/spec/helpers/user_callouts_helper_spec.rb @@ -0,0 +1,47 @@ +require "spec_helper" + +describe UserCalloutsHelper do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + describe '.show_gke_cluster_integration_callout?' do + let(:project) { create(:project) } + + subject { helper.show_gke_cluster_integration_callout?(project) } + + context 'when user can create a cluster' do + before do + allow(helper).to receive(:can?).with(anything, :create_cluster, anything) + .and_return(true) + end + + context 'when user has not dismissed' do + before do + allow(helper).to receive(:user_dismissed?).and_return(false) + end + + it { is_expected.to be true } + end + + context 'when user dismissed' do + before do + allow(helper).to receive(:user_dismissed?).and_return(true) + end + + it { is_expected.to be false } + end + end + + context 'when user can not create a cluster' do + before do + allow(helper).to receive(:can?).with(anything, :create_cluster, anything) + .and_return(false) + end + + it { is_expected.to be false } + end + end +end diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb index fa8cfda3b86..9d4e34abef5 100644 --- a/spec/helpers/version_check_helper_spec.rb +++ b/spec/helpers/version_check_helper_spec.rb @@ -4,7 +4,7 @@ describe VersionCheckHelper do describe '#version_status_badge' do it 'should return nil if not dev environment and not enabled' do allow(Rails.env).to receive(:production?) { false } - allow(helper.current_application_settings).to receive(:version_check_enabled) { false } + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false } expect(helper.version_status_badge).to be(nil) end @@ -12,7 +12,7 @@ describe VersionCheckHelper do context 'when production and enabled' do before do allow(Rails.env).to receive(:production?) { true } - allow(helper.current_application_settings).to receive(:version_check_enabled) { true } + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true } allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' } @image_tag = helper.version_status_badge @@ -27,7 +27,7 @@ describe VersionCheckHelper do end it 'should have a VersionCheck url as the src' do - expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/) + expect(@image_tag).to match(%r{src="https://version\.host\.com/check\.svg\?gitlab_info=xxx"}) end end end diff --git a/spec/initializers/gollum_spec.rb b/spec/initializers/gollum_spec.rb new file mode 100644 index 00000000000..adf824a8947 --- /dev/null +++ b/spec/initializers/gollum_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'gollum' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + let(:gollum_wiki) { Gollum::Wiki.new(wiki.repository.path) } + + before do + create_page(page_name, 'content1') + end + + after do + destroy_page(page_name) + end + + context 'with simple paths' do + let(:page_name) { 'page1' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo/#{page_name}")).to be_nil + end + end + + context 'with complex paths' do + let(:page_name) { '/foo/bar/page2' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo1/bar/page2")).to be_nil + expect(tree_entry("foo/bar1/page2")).to be_nil + end + end + + def tree_entry(name) + gollum_wiki.repo.git.tree_entry(wiki_commits[0].commit, name + '.md') + end + + def wiki_commits + gollum_wiki.repo.commits + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + end + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(name) + page = wiki.find_page(name).page + wiki.delete_page(page, "test commit") + end +end diff --git a/spec/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb new file mode 100644 index 00000000000..2cf5924128f --- /dev/null +++ b/spec/initializers/grape_route_helpers_fix_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require_relative '../../config/initializers/grape_route_helpers_fix' + +describe 'route shadowing' do + include GrapeRouteHelpers::NamedRouteMatcher + + it 'does not occur' do + path = api_v4_projects_merge_requests_path(id: 1) + expect(path).to eq('/api/v4/projects/1/merge_requests') + + path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3) + expect(path).to eq('/api/v4/projects/1/merge_requests/3') + end +end diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb index a11824d0ac5..838ca9fabef 100644 --- a/spec/initializers/settings_spec.rb +++ b/spec/initializers/settings_spec.rb @@ -24,7 +24,7 @@ describe Settings do expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com' - expect(described_class.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + expect(described_class.host_without_www('https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com' diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 2aa4fb1f6c6..cf3a76d0d2e 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Api from '~/api'; describe('Api', () => { @@ -7,20 +9,17 @@ describe('Api', () => { api_version: dummyApiVersion, relative_url_root: dummyUrlRoot, }; - const dummyResponse = 'hello from outer space!'; - const sendDummyResponse = () => { - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; let originalGon; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); originalGon = window.gon; window.gon = Object.assign({}, dummyGon); }); afterEach(() => { + mock.restore(); window.gon = originalGon; }); @@ -38,15 +37,13 @@ describe('Api', () => { describe('group', () => { it('fetches a group', (done) => { const groupId = '123456'; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}.json`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - return sendDummyResponse(); + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; + mock.onGet(expectedUrl).reply(200, { + name: 'test', }); Api.group(groupId, (response) => { - expect(response).toBe(dummyResponse); + expect(response.name).toBe('test'); done(); }); }); @@ -57,19 +54,13 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.groups(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -79,19 +70,13 @@ describe('Api', () => { it('fetches namespaces', (done) => { const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; - const expectedData = { - search: query, - per_page: 20, - }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.namespaces(query, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -103,21 +88,13 @@ describe('Api', () => { const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; window.gon.current_user_id = 1; - const expectedData = Object.assign({ - search: query, - per_page: 20, - membership: true, - simple: true, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.projects(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -126,20 +103,13 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - simple: true, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.projects(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -154,16 +124,16 @@ describe('Api', () => { const expectedData = { label: labelData, }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.type).toEqual('POST'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); + mock.onPost(expectedUrl).reply((config) => { + expect(config.data).toBe(JSON.stringify(expectedData)); + + return [200, { + name: 'test', + }]; }); Api.newLabel(namespace, project, labelData, (response) => { - expect(response).toBe(dummyResponse); + expect(response.name).toBe('test'); done(); }); }); @@ -174,19 +144,13 @@ describe('Api', () => { const groupId = '123456'; const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; - const expectedData = { - search: query, - per_page: 20, - }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.groupProjects(groupId, query, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -197,14 +161,10 @@ describe('Api', () => { const licenseKey = "driver's license"; const data = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.data).toEqual(data); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.licenseText(licenseKey, data, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -214,13 +174,10 @@ describe('Api', () => { it('fetches a gitignore text', (done) => { const gitignoreKey = 'ignore git'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.gitignoreText(gitignoreKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -230,13 +187,10 @@ describe('Api', () => { it('fetches a .gitlab-ci.yml', (done) => { const gitlabCiYmlKey = 'Y CI ML'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.gitlabCiYml(gitlabCiYmlKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -246,13 +200,10 @@ describe('Api', () => { it('fetches a Dockerfile', (done) => { const dockerfileYmlKey = 'a giant whale'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.dockerfileYml(dockerfileYmlKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -262,17 +213,13 @@ describe('Api', () => { it('fetches an issue template', (done) => { const namespace = 'some namespace'; const project = 'some project'; - const templateKey = 'template key'; + const templateKey = ' template #%?.key '; const templateType = 'template type'; - const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - return sendDummyResponse(); - }); + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`; + mock.onGet(expectedUrl).reply(200, 'test'); Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { - expect(error).toBe(null); - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -283,20 +230,14 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.users(query, options) - .then((response) => { - expect(response).toBe(dummyResponse); + .then(({ data }) => { + expect(data.length).toBe(1); + expect(data[0].name).toBe('test'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js index 9eeae474e7d..38d9bba6868 100644 --- a/spec/javascripts/behaviors/secret_values_spec.js +++ b/spec/javascripts/behaviors/secret_values_spec.js @@ -1,16 +1,24 @@ import SecretValues from '~/behaviors/secret_values'; -function generateFixtureMarkup(secrets, isRevealed) { +function generateValueMarkup( + secret, + valueClass = 'js-secret-value', + placeholderClass = 'js-secret-value-placeholder', +) { + return ` + <div class="${placeholderClass}"> + *** + </div> + <div class="hide ${valueClass}"> + ${secret} + </div> + `; +} + +function generateFixtureMarkup(secrets, isRevealed, valueClass, placeholderClass) { return ` <div class="js-secret-container"> - ${secrets.map(secret => ` - <div class="js-secret-value-placeholder"> - *** - </div> - <div class="hide js-secret-value"> - ${secret} - </div> - `).join('')} + ${secrets.map(secret => generateValueMarkup(secret, valueClass, placeholderClass)).join('')} <button class="js-secret-value-reveal-button" data-secret-reveal-status="${isRevealed}" @@ -21,11 +29,25 @@ function generateFixtureMarkup(secrets, isRevealed) { `; } -function setupSecretFixture(secrets, isRevealed) { +function setupSecretFixture( + secrets, + isRevealed, + valueClass = 'js-secret-value', + placeholderClass = 'js-secret-value-placeholder', +) { const wrapper = document.createElement('div'); - wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed); - - const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container')); + wrapper.innerHTML = generateFixtureMarkup( + secrets, + isRevealed, + valueClass, + placeholderClass, + ); + + const secretValues = new SecretValues({ + container: wrapper.querySelector('.js-secret-container'), + valueSelector: `.${valueClass}`, + placeholderSelector: `.${placeholderClass}`, + }); secretValues.init(); return wrapper; @@ -49,7 +71,7 @@ describe('setupSecretValues', () => { expect(revealButton.textContent).toEqual('Hide value'); }); - it('should value hidden initially', () => { + it('should have value hidden initially', () => { const wrapper = setupSecretFixture(secrets, false); const values = wrapper.querySelectorAll('.js-secret-value'); const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); @@ -143,4 +165,64 @@ describe('setupSecretValues', () => { }); }); }); + + describe('with dynamic secrets', () => { + const secrets = ['mysecret123', 'happygoat456', 'tanuki789']; + + it('should toggle values and placeholders', () => { + const wrapper = setupSecretFixture(secrets, false); + // Insert the new dynamic row + wrapper.querySelector('.js-secret-container').insertAdjacentHTML('afterbegin', generateValueMarkup('foobarbazdynamic')); + + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + revealButton.click(); + + expect(values.length).toEqual(4); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(false); + }); + expect(placeholders.length).toEqual(4); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(true); + }); + + revealButton.click(); + + expect(values.length).toEqual(4); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(true); + }); + expect(placeholders.length).toEqual(4); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(false); + }); + }); + }); + + describe('selector options', () => { + const secrets = ['mysecret123']; + + it('should respect `valueSelector` and `placeholderSelector` options', () => { + const valueClass = 'js-some-custom-placeholder-selector'; + const placeholderClass = 'js-some-custom-value-selector'; + + const wrapper = setupSecretFixture(secrets, false, valueClass, placeholderClass); + const values = wrapper.querySelectorAll(`.${valueClass}`); + const placeholders = wrapper.querySelectorAll(`.${placeholderClass}`); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(values.length).toEqual(1); + expect(placeholders.length).toEqual(1); + + revealButton.click(); + + expect(values.length).toEqual(1); + expect(values[0].classList.contains('hide')).toEqual(false); + expect(placeholders.length).toEqual(1); + expect(placeholders[0].classList.contains('hide')).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index c3e67550f05..a143fc827d5 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -1,4 +1,5 @@ -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import renderNotebook from '~/blob/notebook'; describe('iPython notebook renderer', () => { @@ -17,8 +18,11 @@ describe('iPython notebook renderer', () => { }); describe('successful response', () => { - const response = (request, next) => { - next(request.respondWith(JSON.stringify({ + let mock; + + beforeEach((done) => { + mock = new MockAdapter(axios); + mock.onGet('/test').reply(200, { cells: [{ cell_type: 'markdown', source: ['# test'], @@ -31,13 +35,7 @@ describe('iPython notebook renderer', () => { ], outputs: [], }], - }), { - status: 200, - })); - }; - - beforeEach((done) => { - Vue.http.interceptors.push(response); + }); renderNotebook(); @@ -47,9 +45,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, response, - ); + mock.restore(); }); it('does not show loading icon', () => { @@ -86,14 +82,11 @@ describe('iPython notebook renderer', () => { }); describe('error in JSON response', () => { - const response = (request, next) => { - next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', { - status: 200, - })); - }; + let mock; beforeEach((done) => { - Vue.http.interceptors.push(response); + mock = new MockAdapter(axios); + mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' })); renderNotebook(); @@ -103,9 +96,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, response, - ); + mock.restore(); }); it('does not show loading icon', () => { @@ -122,14 +113,11 @@ describe('iPython notebook renderer', () => { }); describe('error getting file', () => { - const response = (request, next) => { - next(request.respondWith('', { - status: 500, - })); - }; + let mock; beforeEach((done) => { - Vue.http.interceptors.push(response); + mock = new MockAdapter(axios); + mock.onGet('/test').reply(500, ''); renderNotebook(); @@ -139,9 +127,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, response, - ); + mock.restore(); }); it('does not show loading icon', () => { diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index cfa6650d85f..892411a6a40 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -1,28 +1,35 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; import BlobViewer from '~/blob/viewer/index'; +import axios from '~/lib/utils/axios_utils'; describe('Blob viewer', () => { let blob; + let mock; + preloadFixtures('snippets/show.html.raw'); beforeEach(() => { + mock = new MockAdapter(axios); + loadFixtures('snippets/show.html.raw'); $('#modal-upload-blob').remove(); blob = new BlobViewer(); - spyOn($, 'ajax').and.callFake(() => { - const d = $.Deferred(); - - d.resolve({ - html: '<div>testing</div>', - }); + mock.onGet('http://test.host/snippets/1.json?viewer=rich').reply(200, { + html: '<div>testing</div>', + }); - return d.promise(); + mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, { + html: '<div>testing</div>', }); + + spyOn(axios, 'get').and.callThrough(); }); afterEach(() => { + mock.restore(); location.hash = ''; }); @@ -30,7 +37,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]') .classList.contains('hidden'), @@ -46,7 +52,6 @@ describe('Blob viewer', () => { new BlobViewer(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]') .classList.contains('hidden'), @@ -64,12 +69,8 @@ describe('Blob viewer', () => { }); asyncClick() + .then(() => asyncClick()) .then(() => { - expect($.ajax).toHaveBeenCalled(); - return asyncClick(); - }) - .then(() => { - expect($.ajax.calls.count()).toBe(1); expect( document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'), ).toBe('true'); @@ -122,7 +123,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( copyButton.classList.contains('disabled'), ).toBeFalsy(); @@ -135,8 +135,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); - expect( copyButton.getAttribute('data-original-title'), ).toBe('Copy source to clipboard'); @@ -171,14 +169,14 @@ describe('Blob viewer', () => { it('sends AJAX request when switching to simple view', () => { blob.switchToViewer('simple'); - expect($.ajax).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalled(); }); it('does not send AJAX request when switching to rich view', () => { blob.switchToViewer('simple'); blob.switchToViewer('rich'); - expect($.ajax.calls.count()).toBe(1); + expect(axios.get.calls.count()).toBe(1); }); }); }); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 2ee3792dd65..f757dadfada 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,9 +1,8 @@ /* global BoardService */ -/* global mockBoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; import boardBlankState from '~/boards/components/board_blank_state'; -import './mock_data'; +import { mockBoardService } from './mock_data'; describe('Boards blank state', () => { let vm; @@ -20,17 +19,15 @@ describe('Boards blank state', () => { reject(); } else { resolve({ - json() { - return [{ - id: 1, - title: 'To Do', - label: { id: 1 }, - }, { - id: 2, - title: 'Doing', - label: { id: 2 }, - }]; - }, + data: [{ + id: 1, + title: 'To Do', + label: { id: 1 }, + }, { + id: 2, + title: 'Doing', + label: { id: 2 }, + }], }); } })); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 8f607899b20..80a598e63bd 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -1,12 +1,11 @@ /* global List */ /* global ListAssignee */ /* global ListLabel */ -/* global listObj */ -/* global boardsMockInterceptor */ /* global BoardService */ -/* global mockBoardService */ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import '~/boards/models/assignee'; import eventHub from '~/boards/eventhub'; @@ -14,13 +13,15 @@ import '~/boards/models/list'; import '~/boards/models/label'; import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; -import './mock_data'; +import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('Board card', () => { let vm; + let mock; beforeEach((done) => { - Vue.http.interceptors.push(boardsMockInterceptor); + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); @@ -54,7 +55,7 @@ describe('Board card', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + mock.restore(); }); it('returns false when detailIssue is empty', () => { diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 6bd00943a8f..a5fcb10b9dd 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -1,11 +1,9 @@ /* global BoardService */ -/* global boardsMockInterceptor */ /* global List */ -/* global listObj */ /* global ListIssue */ -/* global mockBoardService */ import Vue from 'vue'; -import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Sortable from 'vendor/Sortable'; import BoardList from '~/boards/components/board_list'; import eventHub from '~/boards/eventhub'; @@ -13,18 +11,20 @@ import '~/boards/mixins/sortable_default_options'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/stores/boards_store'; -import './mock_data'; +import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; window.Sortable = Sortable; describe('Board list component', () => { + let mock; let component; beforeEach((done) => { const el = document.createElement('div'); document.body.appendChild(el); - Vue.http.interceptors.push(boardsMockInterceptor); + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); gl.IssueBoardsApp = new Vue(); @@ -60,7 +60,7 @@ describe('Board list component', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + mock.restore(); }); it('renders component', () => { @@ -154,6 +154,18 @@ describe('Board list component', () => { }); }); + 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'); + + done(); + }); + }); + it('shows how many more issues to load', (done) => { component.showCount = true; component.list.issuesSize = 20; @@ -172,9 +184,9 @@ describe('Board list component', () => { component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; - for (let i = 0; i < 19; i += 1) { - const issue = component.list.issues[0]; - issue.id += 1; + for (let i = 1; i < 20; i += 1) { + const issue = Object.assign({}, component.list.issues[0]); + issue.id += i; component.list.issues.push(issue); } diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 02e6692dda8..e204985f039 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -1,24 +1,22 @@ -/* global boardsMockInterceptor */ /* global BoardService */ /* global List */ -/* global listObj */ -/* global mockBoardService */ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import boardNewIssue from '~/boards/components/board_new_issue'; import '~/boards/models/list'; -import './mock_data'; +import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('Issue boards new issue form', () => { let vm; let list; + let mock; let newIssueMock; const promiseReturn = { - json() { - return { - iid: 100, - }; + data: { + iid: 100, }, }; @@ -35,7 +33,9 @@ describe('Issue boards new issue form', () => { const BoardNewIssueComp = Vue.extend(boardNewIssue); - Vue.http.interceptors.push(boardsMockInterceptor); + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); gl.IssueBoardsApp = new Vue(); @@ -56,7 +56,10 @@ describe('Issue boards new issue form', () => { .catch(done.fail); }); - afterEach(() => vm.$destroy()); + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); it('calls submit if submit button is clicked', (done) => { spyOn(vm, 'submit').and.callFake(e => e.preventDefault()); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 0e656858182..8411f4dd8a6 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,12 +1,10 @@ /* eslint-disable comma-dangle, one-var, no-unused-vars */ /* global BoardService */ -/* global boardsMockInterceptor */ -/* global listObj */ -/* global listObjDuplicate */ /* global ListIssue */ -/* global mockBoardService */ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; import '~/boards/models/issue'; @@ -15,11 +13,14 @@ import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; -import './mock_data'; +import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('Store', () => { + let mock; + beforeEach(() => { - Vue.http.interceptors.push(boardsMockInterceptor); + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); @@ -34,7 +35,7 @@ describe('Store', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + mock.restore(); }); it('starts with a blank state', () => { diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index 8dacac20cad..19346e305cf 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -1,9 +1,8 @@ -/* global mockBoardService */ import Vue from 'vue'; import '~/boards/services/board_service'; import '~/boards/components/board'; import '~/boards/models/list'; -import '../mock_data'; +import { mockBoardService } from '../mock_data'; describe('Board component', () => { let vm; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 7d430ec35e2..278155c585e 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -1,6 +1,5 @@ /* global ListAssignee */ /* global ListLabel */ -/* global listObj */ /* global ListIssue */ import Vue from 'vue'; @@ -11,7 +10,7 @@ import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/boards_store'; import '~/boards/components/issue_card_inner'; -import './mock_data'; +import { listObj } from './mock_data'; describe('Issue card component', () => { const user = new ListAssignee({ @@ -46,6 +45,9 @@ describe('Issue card component', () => { component = new Vue({ el: document.querySelector('.test-container'), + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, data() { return { list, @@ -54,9 +56,6 @@ describe('Issue card component', () => { rootPath: '/', }; }, - components: { - 'issue-card': gl.issueBoards.IssueCardInner, - }, template: ` <issue-card :issue="issue" diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 41dcb19df3c..dbbe14fe3e0 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -1,7 +1,6 @@ /* eslint-disable comma-dangle */ /* global BoardService */ /* global ListIssue */ -/* global mockBoardService */ import Vue from 'vue'; import '~/boards/models/issue'; @@ -10,7 +9,7 @@ import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; -import './mock_data'; +import { mockBoardService } from './mock_data'; describe('Issue model', () => { let issue; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index eead396ca7e..34964b20b05 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,27 +1,26 @@ /* eslint-disable comma-dangle */ -/* global boardsMockInterceptor */ /* global BoardService */ -/* global mockBoardService */ /* global List */ /* global ListIssue */ -/* global listObj */ -/* global listObjDuplicate */ - -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import _ from 'underscore'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; -import './mock_data'; +import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('List model', () => { let list; + let mock; beforeEach(() => { - Vue.http.interceptors.push(boardsMockInterceptor); + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService({ bulkUpdatePath: '/test/issue-boards/board/1/lists', }); @@ -31,7 +30,7 @@ describe('List model', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + mock.restore(); }); it('gets issues when created', (done) => { @@ -158,10 +157,8 @@ describe('List model', () => { describe('newIssue', () => { beforeEach(() => { spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({ - json() { - return { - id: 42, - }; + data: { + id: 42, }, })); }); diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 0a93086985e..0671facb285 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,20 +1,21 @@ /* global BoardService */ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ +import _ from 'underscore'; -const listObj = { - id: _.random(10000), +export const listObj = { + id: 300, position: 0, title: 'Test', list_type: 'label', label: { - id: _.random(10000), + id: 5000, title: 'Testing', color: 'red', description: 'testing;' } }; -const listObjDuplicate = { +export const listObjDuplicate = { id: listObj.id, position: 1, title: 'Test', @@ -27,9 +28,9 @@ const listObjDuplicate = { } }; -const BoardsMockData = { +export const BoardsMockData = { 'GET': { - '/test/boards/1{/id}/issues': { + '/test/-/boards/1/lists/300/issues?id=300&page=1&=': { issues: [{ title: 'Testing', id: 1, @@ -41,7 +42,7 @@ const BoardsMockData = { } }, 'POST': { - '/test/boards/1{/id}': listObj + '/test/-/boards/1/lists': listObj }, 'PUT': { '/test/issue-boards/board/1/lists{/id}': {} @@ -51,17 +52,14 @@ const BoardsMockData = { } }; -const boardsMockInterceptor = (request, next) => { - const body = BoardsMockData[request.method][request.url]; - - next(request.respondWith(JSON.stringify(body), { - status: 200 - })); +export const boardsMockInterceptor = (config) => { + const body = BoardsMockData[config.method.toUpperCase()][config.url]; + return [200, body]; }; -const mockBoardService = (opts = {}) => { - const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board'; - const listsEndpoint = opts.listsEndpoint || '/test/boards/1'; +export const mockBoardService = (opts = {}) => { + const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/boards.json'; + const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists'; const bulkUpdatePath = opts.bulkUpdatePath || ''; const boardId = opts.boardId || '1'; @@ -72,9 +70,3 @@ const mockBoardService = (opts = {}) => { boardId, }); }; - -window.listObj = listObj; -window.listObjDuplicate = listObjDuplicate; -window.BoardsMockData = BoardsMockData; -window.boardsMockInterceptor = boardsMockInterceptor; -window.mockBoardService = mockBoardService; diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js new file mode 100644 index 00000000000..922215ffc1d --- /dev/null +++ b/spec/javascripts/boards/utils/query_data_spec.js @@ -0,0 +1,27 @@ +import queryData from '~/boards/utils/query_data'; + +describe('queryData', () => { + it('parses path for label with trailing +', () => { + expect( + queryData('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + + it('parses path for milestone with trailing +', () => { + expect( + queryData('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); + + it('parses path for search terms with spaces', () => { + expect( + queryData('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js new file mode 100644 index 00000000000..0170ab458d4 --- /dev/null +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -0,0 +1,163 @@ +import VariableList from '~/ci_variable_list/ci_variable_list'; +import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; + +describe('VariableList', () => { + preloadFixtures('pipeline_schedules/edit.html.raw'); + preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + + let $wrapper; + let variableList; + + describe('with only key/value inputs', () => { + describe('with no variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should remove the row when clicking the remove button', () => { + $wrapper.find('.js-row-remove-button').trigger('click'); + + expect($wrapper.find('.js-row').length).toBe(0); + }); + + it('should add another row when editing the last rows key input', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + expect($keyInput.val()).toBe(''); + }); + + it('should add another row when editing the last rows value textarea', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + expect($valueInput.val()).toBe(''); + }); + + it('should remove empty row after blurring', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + $row.find('.js-ci-variable-input-key') + .val('') + .trigger('input') + .trigger('blur'); + + expect($wrapper.find('.js-row').length).toBe(1); + }); + }); + + describe('with persisted variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should have "Reveal values" button initially when there are already variables', () => { + expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values'); + }); + + it('should reveal hidden values', () => { + const $row = $wrapper.find('.js-row:first-child'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + expect($placeholder.hasClass('hide')).toBe(false); + expect($inputValue.hasClass('hide')).toBe(true); + + // Reveal values + $wrapper.find('.js-secret-value-reveal-button').click(); + + expect($placeholder.hasClass('hide')).toBe(true); + expect($inputValue.hasClass('hide')).toBe(false); + }); + }); + }); + + describe('with all inputs(key, value, protected)', () => { + beforeEach(() => { + // This markup will be replaced with a fixture when we can render the + // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 + $wrapper = $(`<form class="js-variable-list"> + <ul> + <li class="js-row"> + <div class="ci-variable-body-item"> + <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]"> + </div> + + <div class="ci-variable-body-item"> + <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea> + </div> + + <div class="ci-variable-body-item ci-variable-protected-item"> + <button type="button" class="js-project-feature-toggle project-feature-toggle"> + <input + type="hidden" + class="js-ci-variable-input-protected js-project-feature-toggle-input" + name="variables[variables_attributes][][protected]" + value="true" + /> + </button> + </div> + + <button type="button" class="js-row-remove-button"></button> + </li> + </ul> + <button type="button" class="js-secret-value-reveal-button"> + Reveal values + </button> + </form>`); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should add another row when editing the last rows protected checkbox', (done) => { + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-protected-item .js-project-feature-toggle').click(); + + getSetTimeoutPromise() + .then(() => { + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected'); + expect($protectedInput.val()).toBe('true'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js new file mode 100644 index 00000000000..eb508a7f059 --- /dev/null +++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js @@ -0,0 +1,30 @@ +import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; + +describe('NativeFormVariableList', () => { + preloadFixtures('pipeline_schedules/edit.html.raw'); + + let $wrapper; + + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + setupNativeFormVariableList({ + container: $wrapper, + formField: 'schedule', + }); + }); + + describe('onFormSubmit', () => { + it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { + const $row = $wrapper.find('.js-row'); + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]'); + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]'); + + $wrapper.closest('form').trigger('trigger-submit'); + + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(''); + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(''); + }); + }); +}); diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index f5be9ea0fb2..7b38f6b7855 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -23,16 +23,24 @@ describe('Clusters', () => { }); describe('toggle', () => { - it('should update the button and the input field on click', () => { - cluster.toggleButton.click(); + it('should update the button and the input field on click', (done) => { + const toggleButton = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle'); + const toggleInput = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle-input'); - expect( - cluster.toggleButton.classList, - ).not.toContain('is-checked'); + toggleButton.click(); - expect( - cluster.toggleInput.getAttribute('value'), - ).toEqual('false'); + getSetTimeoutPromise() + .then(() => { + expect( + toggleButton.classList, + ).not.toContain('is-checked'); + + expect( + toggleInput.getAttribute('value'), + ).toEqual('false'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js deleted file mode 100644 index 0a8b63ed5b4..00000000000 --- a/spec/javascripts/clusters/clusters_index_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import setClusterTableToggles from '~/clusters/clusters_index'; -import { setTimeout } from 'core-js/library/web/timers'; - -describe('Clusters table', () => { - preloadFixtures('clusters/index_cluster.html.raw'); - let mock; - - beforeEach(() => { - loadFixtures('clusters/index_cluster.html.raw'); - mock = new MockAdapter(axios); - setClusterTableToggles(); - }); - - describe('update cluster', () => { - it('renders loading state while request is made', () => { - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - - expect(button.classList).toContain('is-loading'); - expect(button.getAttribute('disabled')).toEqual('true'); - }); - - afterEach(() => { - mock.restore(); - }); - - it('shows updated state after sucessfull request', (done) => { - mock.onPut().reply(200, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - button.click(); - - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).not.toContain('is-checked'); - done(); - }, 0); - }); - - it('shows inital state after failed request', (done) => { - mock.onPut().reply(500, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).toContain('is-checked'); - done(); - }, 0); - }); - }); -}); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 7460da031c4..1a8affad4e3 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -21,6 +21,7 @@ describe('Applications', () => { helm: { title: 'Helm Tiller' }, ingress: { title: 'Ingress' }, runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, }, }); }); @@ -33,6 +34,10 @@ describe('Applications', () => { expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined(); }); + it('renders a row for Prometheus', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined(); + }); + /* * / it('renders a row for GitLab Runner', () => { expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index af6b6a73819..253b3c45243 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -22,6 +22,11 @@ const CLUSTERS_MOCK_DATA = { name: 'runner', status: APPLICATION_INSTALLING, status_reason: null, + }, + { + name: 'prometheus', + status: APPLICATION_ERROR, + status_reason: 'Cannot connect', }], }, }, @@ -30,6 +35,7 @@ const CLUSTERS_MOCK_DATA = { '/gitlab-org/gitlab-shell/clusters/1/applications/helm': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { }, + '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { }, }, }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index cb8b3d38e2e..ec2889355e6 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -82,6 +82,13 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, }, + prometheus: { + title: 'Prometheus', + status: mockResponseData.applications[3].status, + statusReason: mockResponseData.applications[3].status_reason, + requestStatus: null, + requestReason: null, + }, }, }); }); diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index 5026eaafaca..2abf52a1676 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -1,10 +1,14 @@ /* eslint-disable no-new */ import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Issuable right sidebar collapsed todo toggle', () => { const fixtureName = 'issues/open-issue.html.raw'; const jsonFixtureName = 'todos/todos.json'; + let mock; preloadFixtures(fixtureName); preloadFixtures(jsonFixtureName); @@ -19,19 +23,26 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document.querySelector('.js-right-sidebar') .classList.toggle('right-sidebar-collapsed'); - spyOn(jQuery, 'ajax').and.callFake((res) => { - const d = $.Deferred(); + mock = new MockAdapter(axios); + + mock.onPost(`${gl.TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => { const response = _.clone(todoData); - if (res.type === 'DELETE') { - delete response.delete_path; - } + return [200, response]; + }); - d.resolve(response); - return d.promise(); + mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => { + const response = _.clone(todoData); + delete response.delete_path; + + return [200, response]; }); }); + afterEach(() => { + mock.restore(); + }); + it('shows add todo button', () => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'), @@ -52,71 +63,101 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ).toBe('Add todo'); }); - it('toggle todo state', () => { + it('toggle todo state', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).not.toBeNull(); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'), - ).not.toBeNull(); - }); + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'), + ).not.toBeNull(); - it('toggle todo state of expanded todo toggle', () => { - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - - expect( - document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark done'); + done(); + }); }); - it('toggles todo button tooltip', () => { + it('toggle todo state of expanded todo toggle', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), - ).toBe('Mark done'); - }); - - it('marks todo as done', () => { - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + setTimeout(() => { + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Mark done'); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).not.toBeNull(); + done(); + }); + }); + it('toggles todo button tooltip', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).toBeNull(); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), + ).toBe('Mark done'); - expect( - document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Add todo'); + done(); + }); }); - it('updates aria-label to mark done', () => { + it('marks todo as done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + timeoutPromise() + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + }) + .then(timeoutPromise) + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).toBeNull(); + + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Add todo'); + }) + .then(done) + .catch(done.fail); }); - it('updates aria-label to add todo', () => { + it('updates aria-label to mark done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + done(); + }); + }); + + it('updates aria-label to add todo', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Add todo'); + timeoutPromise() + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + }) + .then(timeoutPromise) + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Add todo'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js new file mode 100644 index 00000000000..90f290e845e --- /dev/null +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit pipeline status component', () => { + let vm; + let Component; + let mock; + const mockCiStatus = { + details_path: '/root/hello-world/pipelines/1', + favicon: 'canceled.ico', + group: 'canceled', + has_details: true, + icon: 'status_canceled', + label: 'canceled', + text: 'canceled', + }; + + beforeEach(() => { + Component = Vue.extend(commitPipelineStatus); + }); + + describe('While polling pipeline data succesfully', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.resolve([200, { + pipelines: [ + { + details: { + status: mockCiStatus, + }, + }, + ], + }]); + return res; + }); + vm = mountComponent(Component, { + endpoint: '/dummy/endpoint', + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('shows the loading icon when polling is starting', (done) => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + setTimeout(() => { + expect(vm.$el.querySelector('.loading-container')).toBe(null); + done(); + }); + }); + + it('contains a ciStatus when the polling is succesful ', (done) => { + setTimeout(() => { + expect(vm.ciStatus).toEqual(mockCiStatus); + done(); + }); + }); + + it('contains a ci-status icon when polling is succesful', (done) => { + setTimeout(() => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); + expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`); + done(); + }); + }); + }); + + describe('When polling data was not succesful', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.reject([502, { }]); + return res; + }); + vm = new Component({ + props: { + endpoint: '/dummy/endpoint', + }, + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('calls an errorCallback', (done) => { + spyOn(vm, 'errorCallback').and.callThrough(); + vm.$mount(); + setTimeout(() => { + expect(vm.errorCallback.calls.count()).toEqual(1); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 9fc047b1f5e..0afe09d87bc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; @@ -9,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTable = Vue.extend(pipelinesTable); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('successful request', () => { diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/javascripts/commit_merge_requests_spec.js new file mode 100644 index 00000000000..3466ef51ea8 --- /dev/null +++ b/spec/javascripts/commit_merge_requests_spec.js @@ -0,0 +1,60 @@ +import * as CommitMergeRequests from '~/commit_merge_requests'; + +describe('CommitMergeRequests', () => { + describe('createContent', () => { + it('should return created content', () => { + const content1 = CommitMergeRequests.createContent([{ iid: 1, path: '/path1', title: 'foo' }, { iid: 2, path: '/path2', title: 'baz' }])[0]; + expect(content1.tagName).toEqual('SPAN'); + expect(content1.childElementCount).toEqual(4); + + const content2 = CommitMergeRequests.createContent([])[0]; + expect(content2.tagName).toEqual('SPAN'); + expect(content2.childElementCount).toEqual(0); + expect(content2.innerText).toEqual('No related merge requests found'); + }); + }); + + describe('getHeaderText', () => { + it('should return header text', () => { + expect(CommitMergeRequests.getHeaderText(0, 1)).toEqual('1 merge request'); + expect(CommitMergeRequests.getHeaderText(0, 2)).toEqual('2 merge requests'); + expect(CommitMergeRequests.getHeaderText(1, 1)).toEqual(','); + expect(CommitMergeRequests.getHeaderText(1, 2)).toEqual(','); + }); + }); + + describe('createHeader', () => { + it('should return created header', () => { + const header = CommitMergeRequests.createHeader(0, 1)[0]; + expect(header.tagName).toEqual('SPAN'); + expect(header.innerText).toEqual('1 merge request'); + }); + }); + + describe('createItem', () => { + it('should return created item', () => { + const item = CommitMergeRequests.createItem({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(item.tagName).toEqual('SPAN'); + expect(item.childElementCount).toEqual(2); + expect(item.children[0].tagName).toEqual('A'); + expect(item.children[1].tagName).toEqual('SPAN'); + }); + }); + + describe('createLink', () => { + it('should return created link', () => { + const link = CommitMergeRequests.createLink({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(link.tagName).toEqual('A'); + expect(link.href).toMatch(/\/path$/); + expect(link.innerText).toEqual('!1'); + }); + }); + + describe('createTitle', () => { + it('should return created title', () => { + const title = CommitMergeRequests.createTitle({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(title.tagName).toEqual('SPAN'); + expect(title.innerText).toEqual('foo'); + }); + }); +}); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index d0176520440..44ec9e4eabf 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,4 +1,6 @@ import 'vendor/jquery.endless-scroll'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; describe('Commits List', () => { @@ -43,30 +45,47 @@ describe('Commits List', () => { describe('on entering input', () => { let ajaxSpy; + let mock; beforeEach(() => { CommitsList.init(25); CommitsList.searchField.val(''); spyOn(history, 'replaceState').and.stub(); - ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { - req.success({ - data: '<li>Result</li>', - }); + mock = new MockAdapter(axios); + + mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, { + html: '<li>Result</li>', }); + + ajaxSpy = spyOn(axios, 'get').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); }); - it('should save the last search string', () => { + it('should save the last search string', (done) => { CommitsList.searchField.val('GitLab'); - CommitsList.filterResults(); - expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); + CommitsList.filterResults() + .then(() => { + expect(ajaxSpy).toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual('GitLab'); + + done(); + }) + .catch(done.fail); }); - it('should not make ajax call if the input does not change', () => { - CommitsList.filterResults(); - expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); + it('should not make ajax call if the input does not change', (done) => { + CommitsList.filterResults() + .then(() => { + expect(ajaxSpy).not.toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual(''); + + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js new file mode 100644 index 00000000000..143137c23ec --- /dev/null +++ b/spec/javascripts/create_item_dropdown_spec.js @@ -0,0 +1,183 @@ +import CreateItemDropdown from '~/create_item_dropdown'; + +const DROPDOWN_ITEM_DATA = [{ + title: 'one', + id: 'one', + text: 'one', +}, { + title: 'two', + id: 'two', + text: 'two', +}, { + title: 'three', + id: 'three', + text: 'three', +}]; + +describe('CreateItemDropdown', () => { + preloadFixtures('static/create_item_dropdown.html.raw'); + + let $wrapperEl; + let createItemDropdown; + + function createItemAndClearInput(text) { + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(text) + .trigger('input'); + + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + // Clear out the filter + $wrapperEl.find('.dropdown-input-field') + .val('') + .trigger('input'); + } + + beforeEach(() => { + loadFixtures('static/create_item_dropdown.html.raw'); + $wrapperEl = $('.js-create-item-dropdown-fixture-root'); + }); + + afterEach(() => { + $wrapperEl.remove(); + }); + + describe('items', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + }); + + it('should have a dropdown item for each piece of data', () => { + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + }); + + describe('created items', () => { + const NEW_ITEM_TEXT = 'foobarbaz'; + + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + + // Open the dropdown + $('.js-dropdown-menu-toggle').click(); + + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(NEW_ITEM_TEXT) + .trigger('input'); + }); + + it('create new item button should include the filter text', () => { + expect($wrapperEl.find('.js-dropdown-create-new-item code').text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should update the dropdown with the newly created item', () => { + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual(NEW_ITEM_TEXT); + expect($wrapperEl.find('input[name="variable[environment]"]').val()).toEqual(NEW_ITEM_TEXT); + }); + + it('should include newly created item in dropdown list', () => { + createItemAndClearInput(NEW_ITEM_TEXT); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); + expect($($itemEls.get(DROPDOWN_ITEM_DATA.length)).text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should not duplicate an item when trying to create an existing item', () => { + createItemAndClearInput(DROPDOWN_ITEM_DATA[0].text); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + }); + + describe('clearDropdown()', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + }); + + it('should clear all data and filter input', () => { + const filterInput = $wrapperEl.find('.dropdown-input-field'); + + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + // Filter for an item + filterInput + .val('one') + .trigger('input'); + + const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a'); + expect($itemElsAfterFilter.length).toEqual(1); + + createItemDropdown.clearDropdown(); + + const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a'); + expect($itemElsAfterClear.length).toEqual(0); + expect(filterInput.val()).toEqual(''); + }); + }); + + describe('createNewItemFromValue option', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + createNewItemFromValue: newValue => ({ + title: `${newValue}-title`, + id: `${newValue}-id`, + text: `${newValue}-text`, + }), + }); + }); + + it('all items go through createNewItemFromValue', () => { + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + createItemAndClearInput('new-item'); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); + expect($($itemEls[3]).text()).toEqual('new-item-text'); + expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title'); + }); + }); +}); diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js index fb6b7fee168..64a76a6ee5f 100644 --- a/spec/javascripts/cycle_analytics/banner_spec.js +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -20,8 +20,9 @@ describe('Cycle analytics banner', () => { expect( vm.$el.querySelector('h4').textContent.trim(), ).toEqual('Introducing Cycle Analytics'); + expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); expect( vm.$el.querySelector('a').textContent.trim(), diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js index 31b65fd1cde..ad0fc38a856 100644 --- a/spec/javascripts/cycle_analytics/total_time_component_spec.js +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -23,7 +23,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs'); }); it('should render information for hours and minutes', () => { @@ -34,7 +34,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins'); }); it('should render information for seconds', () => { @@ -44,7 +44,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('45 s'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s'); }); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 0ca9290d3d2..b870f87eab9 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import eventHub from '~/deploy_keys/eventhub'; import deployKeysApp from '~/deploy_keys/components/app.vue'; diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 2f28c5bbf01..b7aadf604a4 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -53,18 +53,24 @@ describe('Deploy keys key', () => { ).toBe('Remove'); }); - it('shows write access text when key has write access', (done) => { - vm.deployKey.can_push = true; + it('shows write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = true; Vue.nextTick(() => { expect( - vm.$el.querySelector('.write-access-allowed'), - ).not.toBeNull(); - - expect( - vm.$el.querySelector('.write-access-allowed').textContent.trim(), + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), ).toBe('Write access allowed'); + done(); + }); + }); + + it('does not show write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = false; + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), + ).toBe('Read access only'); done(); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0e141adb628..7a34126eef7 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -68,7 +68,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://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 +84,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://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', diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index d02adb25b4e..a41a4e5a3f7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsComponent from '~/environments/components/environments_app.vue'; import { environment, folder } from './mock_data'; diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 4ea4d9d7499..a085074d312 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 5111632d681..b8890e4cda1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -252,6 +252,7 @@ describe('Filtered Search Manager', () => { it('removes last token', () => { spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); dispatchBackspaceEvent(input, 'keyup'); + dispatchBackspaceEvent(input, 'keyup'); expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); }); @@ -259,6 +260,7 @@ describe('Filtered Search Manager', () => { it('sets the input', () => { spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); dispatchDeleteEvent(input, 'keyup'); + dispatchDeleteEvent(input, 'keyup'); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(input.value).toEqual('~bug'); @@ -276,6 +278,18 @@ describe('Filtered Search Manager', () => { expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('text'); }); + + it('does not remove previous token on single backspace press', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + input.value = 't'; + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('t'); + }); }); describe('removeToken', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 2ecb64d84b5..0684c3498a2 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index d26ea3febe8..8e74c4f859c 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end - - context 'rendering non-empty state' do - before do - cluster - end - - it 'clusters/index_cluster.html.raw' do |example| - get :index, - namespace_id: namespace, - project_id: project - - expect(response).to be_success - store_frontend_fixture(response, example.description) - end - end end diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml new file mode 100644 index 00000000000..d4d91b93caf --- /dev/null +++ b/spec/javascripts/fixtures/create_item_dropdown.html.haml @@ -0,0 +1,13 @@ +.js-create-item-dropdown-fixture-root + %input{ name: 'variable[environment]', type: 'hidden' } + = dropdown_tag('some label', + options: { toggle_class: 'js-dropdown-menu-toggle', + content_class: 'js-dropdown-content', + filter: true, + dropdown_class: "dropdown-menu-selectable", + footer_content: true }) do + %ul.dropdown-footer-list + %li + %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" } + Create wildcard + %code diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb new file mode 100644 index 00000000000..56f27ea7df1 --- /dev/null +++ b/spec/javascripts/fixtures/pipeline_schedules.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :public, :repository) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) } + let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) } + + render_views + + before(:all) do + clean_frontend_fixtures('pipeline_schedules/') + end + + before do + sign_in(admin) + end + + it 'pipeline_schedules/edit.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule.id + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'pipeline_schedules/edit_with_variables.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule_populated.id + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 85ee61f0b54..0161c0550d1 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -7,4 +7,6 @@ "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', "has-ci" => 'foo', - "ci-lint-path" => 'foo' } } + "ci-lint-path" => 'foo', + "reset-cache-path" => 'foo' } } + diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 1339ee00870..68a150f602a 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -14,7 +14,7 @@ "username": "root", "id": 1, "state": "active", - "avatar_url": "http://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/u/root" }, "name": "test", diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb new file mode 100644 index 00000000000..703cd3d49fa --- /dev/null +++ b/spec/javascripts/fixtures/search.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe SearchController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + render_views + + before(:all) do + clean_frontend_fixtures('search/') + end + + it 'search/show.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index 97e3ab682c5..7198dbd4cf2 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -183,11 +183,15 @@ describe('Flash', () => { }); it('adds flash element into container', () => { - flash('test'); + flash('test', 'alert', document, null, false, true); expect( document.querySelector('.flash-alert'), ).not.toBeNull(); + + expect( + document.body.className, + ).toContain('flash-shown'); }); it('adds flash into specified parent', () => { @@ -220,13 +224,17 @@ describe('Flash', () => { }); it('removes element after clicking', () => { - flash('test', 'alert', document, null, false); + flash('test', 'alert', document, null, false, true); document.querySelector('.flash-alert').click(); expect( document.querySelector('.flash-alert'), ).toBeNull(); + + expect( + document.body.className, + ).not.toContain('flash-shown'); }); describe('with actionConfig', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index a3fa07d5bc2..eb9330f5e5b 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -167,30 +167,26 @@ describe('Fly out sidebar navigation', () => { describe('mouseEnterTopItems', () => { beforeEach(() => { - jasmine.clock().install(); - el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>'; }); - afterEach(() => { - jasmine.clock().uninstall(); - }); - - it('shows sub-items after 0ms if no menu is open', () => { + it('shows sub-items after 0ms if no menu is open', (done) => { mouseEnterTopItems(el); expect( getHideSubItemsInterval(), ).toBe(0); - jasmine.clock().tick(0); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); - it('shows sub-items after 300ms if a menu is currently open', () => { + it('shows sub-items after 300ms if a menu is currently open', (done) => { documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, @@ -203,17 +199,19 @@ describe('Fly out sidebar navigation', () => { clientY: el.getBoundingClientRect().top + 10, }); - mouseEnterTopItems(el); + mouseEnterTopItems(el, 0); expect( getHideSubItemsInterval(), ).toBe(300); - jasmine.clock().tick(300); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); }); diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 6f357306ec7..50a587ef351 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -130,16 +130,25 @@ describe('GfmAutoComplete', function () { }); describe('should not match special sequences', () => { - const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + const shouldNotBePrependedBy = ['`']; flagsUseDefaultMatcher.forEach((atSign) => { - ShouldNotBeFollowedBy.forEach((followedSymbol) => { + shouldNotBeFollowedBy.forEach((followedSymbol) => { const seq = atSign + followedSymbol; it(`should not match "${seq}"`, () => { expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); }); }); + + shouldNotBePrependedBy.forEach((prependedSymbol) => { + const seq = prependedSymbol + atSign; + + it(`should not match "${seq}"`, () => { + expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); + }); + }); }); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 97e39f6411b..8338efe915b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -256,6 +256,36 @@ describe('AppComponent', () => { }); }); + describe('showLeaveGroupModal', () => { + it('caches candidate group (as props) which is to be left', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.targetGroup).toBe(null); + expect(vm.targetParentGroup).toBe(null); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.targetGroup).not.toBe(null); + expect(vm.targetParentGroup).not.toBe(null); + }); + + it('updates props which show modal confirmation dialog', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.showModal).toBeFalsy(); + expect(vm.groupLeaveConfirmationMessage).toBe(''); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); + }); + }); + + describe('hideLeaveGroupModal', () => { + it('hides modal confirmation which is shown before leaving the group', () => { + const group = Object.assign({}, mockParentGroupItem); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + vm.hideLeaveGroupModal(); + expect(vm.showModal).toBeFalsy(); + }); + }); + describe('leaveGroup', () => { let groupItem; let childGroupItem; @@ -265,21 +295,24 @@ describe('AppComponent', () => { groupItem.children = mockChildren; childGroupItem = groupItem.children[0]; groupItem.isChildrenLoading = false; + vm.targetGroup = childGroupItem; + vm.targetParentGroup = groupItem; }); - it('should leave group and remove group item from tree', (done) => { + it('hides modal confirmation leave group and remove group item from tree', (done) => { const notice = `You left the "${childGroupItem.fullName}" group.`; spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); spyOn($, 'scrollTo'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); - expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + vm.leaveGroup(); + expect(vm.showModal).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); - expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); done(); }, 0); @@ -291,13 +324,13 @@ describe('AppComponent', () => { spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + vm.leaveGroup(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -309,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -364,7 +397,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); newVm.$destroy(); @@ -404,7 +437,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); done(); @@ -439,5 +472,14 @@ describe('AppComponent', () => { done(); }); }); + + it('renders modal confirmation dialog', () => { + vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; + vm.showModal = true; + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); }); }); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 7a5c1da4d1d..acccbe639c4 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -26,38 +26,12 @@ describe('ItemActionsComponent', () => { vm.$destroy(); }); - describe('computed', () => { - describe('leaveConfirmationMessage', () => { - it('should return appropriate string for leave group confirmation', () => { - expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); - }); - }); - }); - describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.modalStatus).toBeFalsy(); - vm.onLeaveGroup(); - expect(vm.modalStatus).toBeTruthy(); - }); - }); - - describe('leaveGroup', () => { - it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { - spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(true); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); - }); - - it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(false); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).not.toHaveBeenCalled(); + vm.onLeaveGroup(); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', vm.group, vm.parentGroup); }); }); }); @@ -78,7 +52,8 @@ describe('ItemActionsComponent', () => { expect(editBtn.getAttribute('href')).toBe(group.editPath); expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); expect(editBtn.dataset.originalTitle).toBe('Edit group'); - expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined(); + expect(editBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings'); newVm.$destroy(); }); @@ -94,17 +69,10 @@ describe('ItemActionsComponent', () => { expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); - expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined(); + expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave'); newVm.$destroy(); }); - - it('should show modal dialog when `modalStatus` is set to `true`', () => { - vm.modalStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).toBeDefined(); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); - }); }); }); diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js index 4310a07e6e6..8faad455825 100644 --- a/spec/javascripts/groups/components/item_caret_spec.js +++ b/spec/javascripts/groups/components/item_caret_spec.js @@ -16,24 +16,20 @@ describe('ItemCaretComponent', () => { describe('template', () => { it('should render component template correctly', () => { const vm = createComponent(); - vm.$mount(); expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length).toBe(1); vm.$destroy(); }); it('should render caret down icon if `isGroupOpen` prop is `true`', () => { const vm = createComponent(true); - vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down'); vm.$destroy(); }); it('should render caret right icon if `isGroupOpen` prop is `false`', () => { const vm = createComponent(); - vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right'); vm.$destroy(); }); }); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js index e200f9f08bd..55a7a713ca6 100644 --- a/spec/javascripts/groups/components/item_stats_spec.js +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -26,7 +26,6 @@ describe('ItemStatsComponent', () => { Object.keys(VISIBILITY_TYPE_ICON).forEach((visibility) => { const item = Object.assign({}, mockParentGroupItem, { visibility }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); vm.$destroy(); }); @@ -41,7 +40,6 @@ describe('ItemStatsComponent', () => { type: ITEM_TYPE.GROUP, }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); vm.$destroy(); }); @@ -54,7 +52,6 @@ describe('ItemStatsComponent', () => { type: ITEM_TYPE.PROJECT, }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); vm.$destroy(); }); @@ -68,13 +65,11 @@ describe('ItemStatsComponent', () => { item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); vm = createComponent(item); - vm.$mount(); expect(vm.isProject).toBeTruthy(); vm.$destroy(); item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); vm = createComponent(item); - vm.$mount(); expect(vm.isProject).toBeFalsy(); vm.$destroy(); }); @@ -87,13 +82,11 @@ describe('ItemStatsComponent', () => { item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); vm = createComponent(item); - vm.$mount(); expect(vm.isGroup).toBeTruthy(); vm.$destroy(); item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); vm = createComponent(item); - vm.$mount(); expect(vm.isGroup).toBeFalsy(); vm.$destroy(); }); @@ -101,57 +94,37 @@ describe('ItemStatsComponent', () => { }); describe('template', () => { - it('should render component template correctly', () => { + it('renders component container element correctly', () => { const vm = createComponent(); - vm.$mount(); - const visibilityIconEl = vm.$el.querySelector('.item-visibility'); - expect(vm.$el.classList.contains('.stats')).toBeDefined(); - expect(visibilityIconEl).toBeDefined(); - expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); - expect(visibilityIconEl.querySelector('i.fa')).toBeDefined(); + expect(vm.$el.classList.contains('stats')).toBeTruthy(); vm.$destroy(); }); - it('should render stat icons if `item.type` is Group', () => { - const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); - const vm = createComponent(item); - vm.$mount(); - - const subgroupIconEl = vm.$el.querySelector('span.number-subgroups'); - expect(subgroupIconEl).toBeDefined(); - expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups'); - expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined(); - expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`); - - const projectsIconEl = vm.$el.querySelector('span.number-projects'); - expect(projectsIconEl).toBeDefined(); - expect(projectsIconEl.dataset.originalTitle).toBe('Projects'); - expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined(); - expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`); - - const membersIconEl = vm.$el.querySelector('span.number-users'); - expect(membersIconEl).toBeDefined(); - expect(membersIconEl.dataset.originalTitle).toBe('Members'); - expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined(); - expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`); + it('renders item visibility icon and tooltip correctly', () => { + const vm = createComponent(); + + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + expect(visibilityIconEl).not.toBe(null); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); vm.$destroy(); }); - it('should render stat icons if `item.type` is Project', () => { + it('renders start count and last updated information for project item correctly', () => { const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT, starCount: 4, }); const vm = createComponent(item); - vm.$mount(); const projectStarIconEl = vm.$el.querySelector('.project-stars'); - expect(projectStarIconEl).toBeDefined(); - expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined(); - expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`); + expect(projectStarIconEl).not.toBe(null); + expect(projectStarIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); + expect(projectStarIconEl.querySelectorAll('.stat-value').length > 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('.last-updated').length > 0).toBeTruthy(); vm.$destroy(); }); diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js new file mode 100644 index 00000000000..e990870aaa6 --- /dev/null +++ b/spec/javascripts/groups/components/item_stats_value_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import itemStatsValueComponent from '~/groups/components/item_stats_value.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => { + const Component = Vue.extend(itemStatsValueComponent); + + return mountComponent(Component, { + title, + cssClass, + iconName, + tooltipPlacement, + value, + }); +}; + +describe('ItemStatsValueComponent', () => { + describe('computed', () => { + let vm; + const itemConfig = { + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + }; + + describe('isValuePresent', () => { + it('returns true if non-empty `value` is present', () => { + vm = createComponent(Object.assign({}, itemConfig, { value: 10 })); + expect(vm.isValuePresent).toBeTruthy(); + }); + + it('returns false if empty `value` is present', () => { + vm = createComponent(itemConfig); + expect(vm.isValuePresent).toBeFalsy(); + }); + + afterEach(() => { + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + let vm; + beforeEach(() => { + vm = createComponent({ + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + value: 10, + }); + }); + + it('renders component element correctly', () => { + expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length > 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('.stat-value').length > 0).toBeTruthy(); + }); + + it('renders element tooltip correctly', () => { + expect(vm.$el.dataset.originalTitle).toBe('Subgroups'); + expect(vm.$el.dataset.placement).toBe('left'); + }); + + it('renders element icon correctly', () => { + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder'); + }); + + it('renders value count correctly', () => { + expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10'); + }); + + afterEach(() => { + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js index 528e6ed1b4c..495cc97b475 100644 --- a/spec/javascripts/groups/components/item_type_icon_spec.js +++ b/spec/javascripts/groups/components/item_type_icon_spec.js @@ -28,12 +28,12 @@ describe('ItemTypeIconComponent', () => { vm = createComponent(ITEM_TYPE.GROUP, true); vm.$mount(); - expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined(); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open'); vm.$destroy(); vm = createComponent(ITEM_TYPE.GROUP); vm.$mount(); - expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined(); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder'); vm.$destroy(); }); @@ -42,12 +42,12 @@ describe('ItemTypeIconComponent', () => { vm = createComponent(ITEM_TYPE.PROJECT); vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark'); vm.$destroy(); vm = createComponent(ITEM_TYPE.GROUP); vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark'); vm.$destroy(); }); }); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index 6184d671790..8bf6417487d 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -18,9 +18,9 @@ export const PROJECT_VISIBILITY_TYPE = { }; export const VISIBILITY_TYPE_ICON = { - public: 'fa-globe', - internal: 'fa-shield', - private: 'fa-lock', + public: 'earth', + internal: 'shield', + private: 'lock', }; export const mockParentGroupItem = { @@ -46,6 +46,7 @@ export const mockParentGroupItem = { isOpen: true, isChildrenLoading: false, isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', }; export const mockRawChildren = [ @@ -69,6 +70,7 @@ export const mockRawChildren = [ subgroup_count: 2, can_leave: false, children: [], + updated_at: '2017-04-09T18:40:39.101Z', }, ]; @@ -96,6 +98,7 @@ export const mockChildren = [ isOpen: true, isChildrenLoading: false, isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', }, ]; @@ -119,6 +122,7 @@ export const mockGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 67, @@ -139,6 +143,7 @@ export const mockGroups = [ project_count: 0, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 54, @@ -159,6 +164,7 @@ export const mockGroups = [ project_count: 0, subgroup_count: 1, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 5, @@ -179,6 +185,7 @@ export const mockGroups = [ project_count: 1, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 4, @@ -199,6 +206,7 @@ export const mockGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 3, @@ -219,6 +227,7 @@ export const mockGroups = [ project_count: 1, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 2, @@ -239,6 +248,7 @@ export const mockGroups = [ project_count: 4, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, ]; @@ -262,6 +272,7 @@ export const mockSearchedGroups = [ project_count: 1, subgroup_count: 2, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 57, @@ -282,6 +293,7 @@ export const mockSearchedGroups = [ project_count: 4, subgroup_count: 2, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 60, @@ -302,6 +314,7 @@ export const mockSearchedGroups = [ project_count: 0, subgroup_count: 1, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 61, @@ -322,6 +335,7 @@ export const mockSearchedGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 17, @@ -336,6 +350,7 @@ export const mockSearchedGroups = [ permission: null, edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit', star_count: 0, + updated_at: '2017-09-12T06:37:04.925Z', }, { id: 16, @@ -350,6 +365,7 @@ export const mockSearchedGroups = [ permission: null, edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit', star_count: 0, + updated_at: '2017-04-09T18:41:03.112Z', }, ], }, diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js index 686b8eaed31..1415ffb7eb3 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ b/spec/javascripts/helpers/class_spec_helper_spec.js @@ -3,7 +3,7 @@ import './class_spec_helper'; describe('ClassSpecHelper', () => { - describe('itShouldBeAStaticMethod', function () { + describe('itShouldBeAStaticMethod', () => { beforeEach(() => { class TestClass { instanceMethod() { this.prop = 'val'; } @@ -14,23 +14,5 @@ describe('ClassSpecHelper', () => { }); ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); - - it('should have a defined spec', () => { - expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method'); - }); - - it('should pass for a static method', () => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod'); - expect(spec.status()).toBe('passed'); - }); - - it('should fail for an instance method', (done) => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod'); - spec.resultCallback = (result) => { - expect(result.status).toBe('failed'); - done(); - }; - spec.execute(); - }); }); }); diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js index a9783ea065c..323fee3767e 100644 --- a/spec/javascripts/helpers/user_mock_data_helper.js +++ b/spec/javascripts/helpers/user_mock_data_helper.js @@ -4,7 +4,7 @@ export default { for (let i = 0; i < numberUsers; i = i += 1) { users.push( { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: (i + 1), name: `GitLab User ${i}`, username: `gitlab${i}`, diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 9033eb9ce02..d0fba908e34 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -1,3 +1,5 @@ +import MockAdaptor from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import IntegrationSettingsForm from '~/integrations/integration_settings_form'; describe('IntegrationSettingsForm', () => { @@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => { describe('testSettings', () => { let integrationSettingsForm; let formData; + let mock; beforeEach(() => { + mock = new MockAdaptor(axios); + + spyOn(axios, 'put').and.callThrough(); + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); formData = integrationSettingsForm.$form.serialize(); }); - it('should make an ajax request with provided `formData`', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); - integrationSettingsForm.testSettings(formData); + it('should make an ajax request with provided `formData`', (done) => { + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData); - expect($.ajax).toHaveBeenCalledWith({ - type: 'PUT', - url: integrationSettingsForm.testEndPoint, - data: formData, - }); + done(); + }) + .catch(done.fail); }); - it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { + it('should show error Flash with `Save anyway` action if ajax request responds with error in test', (done) => { const errorMessage = 'Test failed.'; - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: true, + message: errorMessage, + service_response: 'some error', + }); - deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' }); + integrationSettingsForm.testSettings(formData) + .then(() => { + const $flashContainer = $('.flash-container'); + expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); + expect($flashContainer.find('.flash-action')).toBeDefined(); + expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway'); - const $flashContainer = $('.flash-container'); - expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); - expect($flashContainer.find('.flash-action')).toBeDefined(); - expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway'); + done(); + }) + .catch(done.fail); }); - it('should submit form if ajax request responds without any error in test', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should submit form if ajax request responds without any error in test', (done) => { + spyOn(integrationSettingsForm.$form, 'submit'); - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: false, + }); - spyOn(integrationSettingsForm.$form, 'submit'); - deferred.resolve({ error: false }); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); - expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); }); - it('should submit form when clicked on `Save anyway` action of error Flash', () => { - const errorMessage = 'Test failed.'; - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should submit form when clicked on `Save anyway` action of error Flash', (done) => { + spyOn(integrationSettingsForm.$form, 'submit'); - integrationSettingsForm.testSettings(formData); + const errorMessage = 'Test failed.'; + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: true, + message: errorMessage, + }); - deferred.resolve({ error: true, message: errorMessage }); + integrationSettingsForm.testSettings(formData) + .then(() => { + const $flashAction = $('.flash-container .flash-action'); + expect($flashAction).toBeDefined(); - const $flashAction = $('.flash-container .flash-action'); - expect($flashAction).toBeDefined(); + $flashAction.get(0).click(); + }) + .then(() => { + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); - spyOn(integrationSettingsForm.$form, 'submit'); - $flashAction.get(0).click(); - expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); }); - it('should show error Flash if ajax request failed', () => { + it('should show error Flash if ajax request failed', (done) => { const errorMessage = 'Something went wrong on our end.'; - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).networkError(); - deferred.reject(); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); - expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); + done(); + }) + .catch(done.fail); }); - it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - integrationSettingsForm.testSettings(formData); + it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => { + mock.onPut(integrationSettingsForm.testEndPoint).networkError(); spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); - deferred.reject(); - expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 5a9112716f4..d53ffecbd35 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -1,3 +1,5 @@ +import MockAdaptor from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import IssuableIndex from '~/issuable_index'; describe('Issuable', () => { @@ -19,6 +21,8 @@ describe('Issuable', () => { }); describe('resetIncomingEmailToken', () => { + let mock; + beforeEach(() => { const element = document.createElement('a'); element.classList.add('incoming-email-token-reset'); @@ -30,14 +34,28 @@ describe('Issuable', () => { document.body.appendChild(input); Issuable = new IssuableIndex('issue_'); + + mock = new MockAdaptor(axios); + + mock.onPut('foo').reply(200, { + new_address: 'testing123', + }); }); - it('should send request to reset email token', () => { - spyOn(jQuery, 'ajax').and.callThrough(); + afterEach(() => { + mock.restore(); + }); + + it('should send request to reset email token', (done) => { + spyOn(axios, 'put').and.callThrough(); document.querySelector('.incoming-email-token-reset').click(); - expect(jQuery.ajax).toHaveBeenCalled(); - expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo'); + setTimeout(() => { + expect(axios.put).toHaveBeenCalledWith('foo'); + expect($('#issuable_email').val()).toBe('testing123'); + + done(); + }); }); }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 7159148f8fa..1c9f48028f2 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import '~/render_math'; import '~/render_gfm'; import * as urlUtils from '~/lib/utils/url_utility'; @@ -11,26 +13,29 @@ function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); } +const REALTIME_REQUEST_STACK = [ + issueShowData.initialRequest, + issueShowData.secondRequest, +]; + describe('Issuable output', () => { - let requestData = issueShowData.initialRequest; + let mock; + let realtimeRequestCount = 0; + let vm; document.body.innerHTML = '<span id="task_status"></span>'; - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(requestData), { - status: 200, - })); - }; - - let vm; - beforeEach((done) => { spyOn(eventHub, '$emit'); const IssuableDescriptionComponent = Vue.extend(issuableApp); - requestData = issueShowData.initialRequest; - Vue.http.interceptors.push(interceptor); + mock = new MockAdapter(axios); + mock.onGet('/gitlab-org/gitlab-shell/issues/9/realtime_changes/realtime_changes').reply(() => { + const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]); + realtimeRequestCount += 1; + return res; + }); vm = new IssuableDescriptionComponent({ propsData: { @@ -54,10 +59,10 @@ describe('Issuable output', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + mock.restore(); + realtimeRequestCount = 0; vm.poll.stop(); - vm.$destroy(); }); @@ -77,7 +82,6 @@ describe('Issuable output', () => { expect(editedText.querySelector('time')).toBeTruthy(); }) .then(() => { - requestData = issueShowData.secondRequest; vm.poll.makeRequest(); }) .then(() => new Promise(resolve => setTimeout(resolve))) @@ -141,24 +145,19 @@ describe('Issuable output', () => { spyOn(vm.service, 'getData').and.callThrough(); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ - json() { - return { - confidential: false, - web_url: location.pathname, - }; + data: { + confidential: false, + web_url: location.pathname, }, }); })); - vm.updateIssuable(); - - setTimeout(() => { - expect( - vm.service.getData, - ).toHaveBeenCalled(); - - done(); - }); + vm.updateIssuable() + .then(() => { + expect(vm.service.getData).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); it('correctly updates issuable data', (done) => { @@ -166,29 +165,22 @@ describe('Issuable output', () => { resolve(); })); - vm.updateIssuable(); - - setTimeout(() => { - expect( - vm.service.updateIssuable, - ).toHaveBeenCalledWith(vm.formState); - expect( - eventHub.$emit, - ).toHaveBeenCalledWith('close.form'); - - done(); - }); + vm.updateIssuable() + .then(() => { + expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState); + expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); + }) + .then(done) + .catch(done.fail); }); it('does not redirect if issue has not moved', (done) => { spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ - json() { - return { - web_url: location.pathname, - confidential: vm.isConfidential, - }; + data: { + web_url: location.pathname, + confidential: vm.isConfidential, }, }); })); @@ -208,11 +200,9 @@ describe('Issuable output', () => { spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ - json() { - return { - web_url: '/testing-issue-move', - confidential: vm.isConfidential, - }; + data: { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, }, }); })); @@ -228,6 +218,39 @@ describe('Issuable output', () => { }); }); + describe('shows dialog when issue has unsaved changed', () => { + it('confirms on title change', (done) => { + vm.showForm = true; + vm.state.titleText = 'title has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('confirms on description change', (done) => { + vm.showForm = true; + vm.state.descriptionText = 'description has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('does nothing when nothing has changed', (done) => { + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).toBeNull(); + done(); + }); + }); + }); + describe('error when updating', () => { beforeEach(() => { spyOn(window, 'Flash').and.callThrough(); @@ -283,10 +306,8 @@ describe('Issuable output', () => { let modal; const promise = new Promise((resolve) => { resolve({ - json() { - return { - recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', - }; + data: { + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', }, }); }); @@ -323,8 +344,8 @@ describe('Issuable output', () => { spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ - json() { - return { web_url: '/test' }; + data: { + web_url: '/test', }, }); })); @@ -345,8 +366,8 @@ describe('Issuable output', () => { spyOn(vm.poll, 'stop').and.callThrough(); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ - json() { - return { web_url: '/test' }; + data: { + web_url: '/test', }, }); })); @@ -385,22 +406,21 @@ describe('Issuable output', () => { describe('open form', () => { it('shows locked warning if form is open & data is different', (done) => { - Vue.nextTick() + vm.$nextTick() .then(() => { vm.openForm(); - requestData = issueShowData.secondRequest; vm.poll.makeRequest(); }) - .then(() => new Promise(resolve => setTimeout(resolve))) + // Wait for the request + .then(vm.$nextTick) + // Wait for the successCallback to update the store state + .then(vm.$nextTick) + // Wait for the new state to flow to the Vue components + .then(vm.$nextTick) .then(() => { - expect( - vm.formState.lockedWarningVisible, - ).toBeTruthy(); - - expect( - vm.$el.querySelector('.alert'), - ).not.toBeNull(); + expect(vm.formState.lockedWarningVisible).toEqual(true); + expect(vm.$el.querySelector('.alert')).not.toBeNull(); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js index 2b7ee65094b..30441faf844 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Issue description template component', () => { let vm; diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 000b53af016..50ce019c32a 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import formComponent from '~/issue_show/components/form.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Inline edit form component', () => { let vm; diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index eb3111412a7..74b3efb014b 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -19,14 +19,4 @@ export default { updated_by_name: 'Other User', updated_by_path: '/other_user', }, - issueSpecRequest: { - title: '<p>this is a title</p>', - title_text: 'this is a title', - description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>', - description_text: '- [ ] Task List Item', - task_status: '0 of 1 completed', - updated_at: '2017-05-15T12:31:04.428Z', - updated_by_name: 'Last User', - updated_by_path: '/last_user', - }, }; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 2cd2e63b15d..177962ecf82 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Issue from '~/issue'; import '~/lib/utils/text_utility'; @@ -68,40 +70,27 @@ describe('Issue', function() { expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue'); } - describe('task lists', function() { - beforeEach(function() { - loadFixtures('issues/issue-with-task-list.html.raw'); - this.issue = new Issue(); - }); - - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template - expect(req.data.issue.description).not.toBe(null); - }); - - $('.js-task-list-field').trigger('tasklist:changed'); - }); - }); - [true, false].forEach((isIssueInitiallyOpen) => { describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() { const action = isIssueInitiallyOpen ? 'close' : 'reopen'; + let mock; - function ajaxSpy(req) { - if (req.url === this.$triggeredButton.attr('href')) { - expect(req.type).toBe('PUT'); - expectNewBranchButtonState(true, false); - return this.issueStateDeferred; - } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) { - expect(req.type).toBe('GET'); + function mockCloseButtonResponseSuccess(url, response) { + mock.onPut(url).reply(() => { expectNewBranchButtonState(true, false); - return this.canCreateBranchDeferred; - } - expect(req.url).toBe('unexpected'); - return null; + return [200, response]; + }); + } + + function mockCloseButtonResponseError(url) { + mock.onPut(url).networkError(); + } + + function mockCanCreateBranch(canCreateBranch) { + mock.onGet(/(.*)\/can_create_branch$/).reply(200, { + can_create_branch: canCreateBranch, + }); } beforeEach(function() { @@ -111,6 +100,11 @@ describe('Issue', function() { loadFixtures('issues/closed-issue.html.raw'); } + mock = new MockAdapter(axios); + + mock.onGet(/(.*)\/related_branches$/).reply(200, {}); + mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {}); + findElements(isIssueInitiallyOpen); this.issue = new Issue(); expectIssueState(isIssueInitiallyOpen); @@ -120,71 +114,89 @@ describe('Issue', function() { this.$projectIssuesCounter = $('.issue_counter').first(); this.$projectIssuesCounter.text('1,001'); - this.issueStateDeferred = new jQuery.Deferred(); - this.canCreateBranchDeferred = new jQuery.Deferred(); + spyOn(axios, 'get').and.callThrough(); + }); - spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this)); + afterEach(() => { + mock.restore(); + $('div.flash-alert').remove(); }); - it(`${action}s the issue`, function() { - this.$triggeredButton.trigger('click'); - this.issueStateDeferred.resolve({ + it(`${action}s the issue`, function(done) { + mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), { id: 34 }); - this.canCreateBranchDeferred.resolve({ - can_create_branch: !isIssueInitiallyOpen - }); + mockCanCreateBranch(!isIssueInitiallyOpen); - expectIssueState(!isIssueInitiallyOpen); - expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); - expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); - expectNewBranchButtonState(false, !isIssueInitiallyOpen); + this.$triggeredButton.trigger('click'); + + setTimeout(() => { + expectIssueState(!isIssueInitiallyOpen); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); + expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); + expectNewBranchButtonState(false, !isIssueInitiallyOpen); + + done(); + }); }); - it(`fails to ${action} the issue if saved:false`, function() { - this.$triggeredButton.trigger('click'); - this.issueStateDeferred.resolve({ + it(`fails to ${action} the issue if saved:false`, function(done) { + mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), { saved: false }); - this.canCreateBranchDeferred.resolve({ - can_create_branch: isIssueInitiallyOpen - }); + mockCanCreateBranch(isIssueInitiallyOpen); - expectIssueState(isIssueInitiallyOpen); - expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); - expectErrorMessage(); - expect(this.$projectIssuesCounter.text()).toBe('1,001'); - expectNewBranchButtonState(false, isIssueInitiallyOpen); + this.$triggeredButton.trigger('click'); + + setTimeout(() => { + expectIssueState(isIssueInitiallyOpen); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); + expectErrorMessage(); + expect(this.$projectIssuesCounter.text()).toBe('1,001'); + expectNewBranchButtonState(false, isIssueInitiallyOpen); + + done(); + }); }); - it(`fails to ${action} the issue if HTTP error occurs`, function() { + it(`fails to ${action} the issue if HTTP error occurs`, function(done) { + mockCloseButtonResponseError(this.$triggeredButton.attr('href')); + mockCanCreateBranch(isIssueInitiallyOpen); + this.$triggeredButton.trigger('click'); - this.issueStateDeferred.reject(); - this.canCreateBranchDeferred.resolve({ - can_create_branch: isIssueInitiallyOpen - }); - expectIssueState(isIssueInitiallyOpen); - expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); - expectErrorMessage(); - expect(this.$projectIssuesCounter.text()).toBe('1,001'); - expectNewBranchButtonState(false, isIssueInitiallyOpen); + setTimeout(() => { + expectIssueState(isIssueInitiallyOpen); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); + expectErrorMessage(); + expect(this.$projectIssuesCounter.text()).toBe('1,001'); + expectNewBranchButtonState(false, isIssueInitiallyOpen); + + done(); + }); }); it('disables the new branch button if Ajax call fails', function() { + mockCloseButtonResponseError(this.$triggeredButton.attr('href')); + mock.onGet(/(.*)\/can_create_branch$/).networkError(); + this.$triggeredButton.trigger('click'); - this.issueStateDeferred.reject(); - this.canCreateBranchDeferred.reject(); expectNewBranchButtonState(false, false); }); - it('does not trigger Ajax call if new branch button is missing', function() { + it('does not trigger Ajax call if new branch button is missing', function(done) { + mockCloseButtonResponseError(this.$triggeredButton.attr('href')); Issue.$btnNewBranch = $(); this.canCreateBranchDeferred = null; this.$triggeredButton.trigger('click'); - this.issueStateDeferred.reject(); + + setTimeout(() => { + expect(axios.get).not.toHaveBeenCalled(); + + done(); + }); }); }); }); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 4f06237deb5..03b58e9c1d0 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,4 +1,6 @@ -import { bytesToKiB } from '~/lib/utils/number_utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import '~/lib/utils/datetime_utility'; import Job from '~/job'; @@ -6,11 +8,29 @@ import '~/breakpoints'; describe('Job', () => { const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; + let mock; + let response; + + function waitForPromise() { + return new Promise(resolve => requestAnimationFrame(resolve)); + } preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { loadFixtures('builds/build-with-artifacts.html.raw'); + + spyOn(urlUtils, 'visitUrl'); + + mock = new MockAdapter(axios); + + mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); + }); + + afterEach(() => { + mock.restore(); + + response = {}; }); describe('class constructor', () => { @@ -52,178 +72,162 @@ describe('Job', () => { expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - - it('displays the remove date correctly', () => { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); - }); }); describe('running build', () => { - it('updates the build trace on an interval', function () { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('updates the build trace on an interval', function (done) { + response = { html: '<span>Update<span>', status: 'running', state: 'newstate', append: true, complete: false, - }); - - deferred2.resolve(); - - deferred3.resolve({ - html: '<span>More</span>', - status: 'running', - state: 'finalstate', - append: true, - complete: true, - }); + }; this.job = new Job(); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.job.state).toBe('newstate'); - - jasmine.clock().tick(4001); - - expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.job.state).toBe('finalstate'); + waitForPromise() + .then(() => { + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + expect(this.job.state).toBe('newstate'); + + response = { + html: '<span>More</span>', + status: 'running', + state: 'finalstate', + append: true, + complete: true, + }; + }) + .then(() => jasmine.clock().tick(4001)) + .then(waitForPromise) + .then(() => { + expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); + expect(this.job.state).toBe('finalstate'); + }) + .then(done) + .catch(done.fail); }); - it('replaces the entire build trace', () => { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('replaces the entire build trace', (done) => { + response = { html: '<span>Update<span>', status: 'running', append: false, complete: false, - }); - - deferred2.resolve(); - - deferred3.resolve({ - html: '<span>Different</span>', - status: 'running', - append: false, - }); + }; this.job = new Job(); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - - jasmine.clock().tick(4001); - - expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); - expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + waitForPromise() + .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(waitForPromise) + .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', () => { - spyOn(urlUtils, 'visitUrl'); - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - deferred.resolve({ + it('shows information about truncated log', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); - expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + waitForPromise() + .then(() => { + expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + }) + .then(done) + .catch(done.fail); }); - it('shows the size in KiB', () => { + it('shows the size in KiB', (done) => { const size = 50; - spyOn(urlUtils, 'visitUrl'); - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + response = { html: '<span>Update</span>', status: 'success', append: false, size, total: 100, - }); + }; this.job = new Job(); - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(size)}`); + waitForPromise() + .then(() => { + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${numberToHumanSize(size)}`); + }) + .then(done) + .catch(done.fail); }); - it('shows incremented size', () => { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('shows incremented size', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); - - deferred2.resolve(); + }; this.job = new Job(); - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(50)}`); - - jasmine.clock().tick(4001); - - deferred3.resolve({ - html: '<span>Update</span>', - status: 'success', - append: true, - size: 10, - total: 100, - }); - - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(60)}`); + waitForPromise() + .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, + }; + }) + .then(() => jasmine.clock().tick(4001)) + .then(waitForPromise) + .then(() => { + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${numberToHumanSize(60)}`); + }) + .then(done) + .catch(done.fail); }); it('renders the raw link', () => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); @@ -234,50 +238,50 @@ describe('Job', () => { }); describe('when size is equal than total', () => { - it('does not show the trunctated information', () => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + it('does not show the trunctated information', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 100, total: 100, - }); + }; this.job = new Job(); - expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + waitForPromise() + .then(() => { + expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + }) + .then(done) + .catch(done.fail); }); }); }); describe('output trace', () => { - beforeEach(() => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + beforeEach((done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); + + waitForPromise() + .then(done) + .catch(done.fail); }); it('should render trace controls', () => { const controllers = document.querySelector('.controllers'); - expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined(); - expect(controllers.querySelector('.js-erase-link')).toBeDefined(); - expect(controllers.querySelector('.js-scroll-up')).toBeDefined(); - expect(controllers.querySelector('.js-scroll-down')).toBeDefined(); + 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', () => { @@ -290,13 +294,13 @@ describe('Job', () => { describe('getBuildTrace', () => { it('should request build trace with state parameter', (done) => { - spyOn(jQuery, 'ajax').and.callThrough(); + spyOn(axios, 'get').and.callThrough(); // eslint-disable-next-line no-new new Job(); setTimeout(() => { - expect(jQuery.ajax).toHaveBeenCalledWith( - { url: `${JOB_URL}/trace.json`, data: { state: '' } }, + expect(axios.get).toHaveBeenCalledWith( + `${JOB_URL}/trace.json`, { params: { state: '' } }, ); done(); }, 0); diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index 4a210faa017..a9df0418d5d 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import headerComponent from '~/jobs/components/header.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Job details header', () => { let HeaderComponent; @@ -30,27 +31,45 @@ describe('Job details header', () => { email: 'foo@bar.com', avatar_url: 'link', }, + started: '2018-01-08T09:48:27.319Z', new_issue_path: 'path', }, isLoading: false, }; - vm = new HeaderComponent({ propsData: props }).$mount(); + vm = mountComponent(HeaderComponent, props); }); afterEach(() => { vm.$destroy(); }); - it('should render provided job information', () => { - expect( - vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), - ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + describe('triggered job', () => { + beforeEach(() => { + vm = mountComponent(HeaderComponent, props); + }); + + it('should render provided job information', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + }); + + it('should render new issue link', () => { + expect( + vm.$el.querySelector('.js-new-issue').getAttribute('href'), + ).toEqual(props.job.new_issue_path); + }); }); - it('should render new issue link', () => { - expect( - vm.$el.querySelector('.js-new-issue').getAttribute('href'), - ).toEqual(props.job.new_issue_path); + describe('created job', () => { + it('should render created key', () => { + props.job.started = false; + vm = mountComponent(HeaderComponent, props); + + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 created 3 weeks ago by Foo'); + }); }); }); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 3069a0cd60e..ca5c9cf87e4 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -12,6 +12,10 @@ describe('JobMediator', () => { mock = new MockAdapter(axios); }); + afterEach(() => { + mock.restore(); + }); + it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); @@ -24,10 +28,6 @@ describe('JobMediator', () => { mock.onGet().reply(200, job, {}); }); - afterEach(() => { - mock.restore(); - }); - it('should store received data', (done) => { mediator.fetchJob(); setTimeout(() => { diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 43532275121..43589d54be4 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -37,7 +37,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://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', }, erase_path: '/root/ci-mock/-/jobs/4757/erase', @@ -54,7 +54,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://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', }, active: false, @@ -107,10 +107,10 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://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', }, - author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + author_gravatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', }, diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js index a197b35f6fb..7d992f62f64 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/javascripts/labels_issue_sidebar_spec.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import IssuableContext from '~/issuable_context'; import LabelsSelect from '~/labels_select'; @@ -10,35 +12,44 @@ import '~/users_select'; (() => { let saveLabelCount = 0; + let mock; + describe('Issue dropdown sidebar', () => { preloadFixtures('static/issue_sidebar_label.html.raw'); beforeEach(() => { loadFixtures('static/issue_sidebar_label.html.raw'); + + mock = new MockAdapter(axios); + new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new LabelsSelect(); - spyOn(jQuery, 'ajax').and.callFake((req) => { - const d = $.Deferred(); - let LABELS_DATA = []; + mock.onGet('/root/test/labels.json').reply(() => { + const labels = Array(10).fill().map((_, i) => ({ + id: i, + title: `test ${i}`, + color: '#5CB85C', + })); - if (req.url === '/root/test/labels.json') { - for (let i = 0; i < 10; i += 1) { - LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); - } - } else if (req.url === '/root/test/issues/2.json') { - const tmp = []; - for (let i = 0; i < saveLabelCount; i += 1) { - tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); - } - LABELS_DATA = { labels: tmp }; - } + return [200, labels]; + }); + + mock.onPut('/root/test/issues/2.json').reply(() => { + const labels = Array(saveLabelCount).fill().map((_, i) => ({ + id: i, + title: `test ${i}`, + color: '#5CB85C', + })); - d.resolve(LABELS_DATA); - return d.promise(); + return [200, { labels }]; }); }); + afterEach(() => { + mock.restore(); + }); + it('changes collapsed tooltip when changing labels when less than 5', (done) => { saveLabelCount = 5; $('.edit-link').get(0).click(); diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js index 49971bd91e2..7603400b55e 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/javascripts/lib/utils/ajax_cache_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import AjaxCache from '~/lib/utils/ajax_cache'; describe('AjaxCache', () => { @@ -87,66 +89,53 @@ describe('AjaxCache', () => { }); describe('retrieve', () => { - let ajaxSpy; + let mock; beforeEach(() => { - spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url)); + mock = new MockAdapter(axios); + + spyOn(axios, 'get').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); }); it('stores and returns data from Ajax call if cache is empty', (done) => { - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; + mock.onGet(dummyEndpoint).reply(200, dummyResponse); AjaxCache.retrieve(dummyEndpoint) .then((data) => { - expect(data).toBe(dummyResponse); - expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse); + expect(data).toEqual(dummyResponse); + expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); }) .then(done) .catch(fail); }); - it('makes no Ajax call if request is pending', () => { - const responseDeferred = $.Deferred(); - - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - // neither reject nor resolve to keep request pending - return responseDeferred.promise(); - }; - - const unexpectedResponse = data => fail(`Did not expect response: ${data}`); + it('makes no Ajax call if request is pending', (done) => { + mock.onGet(dummyEndpoint).reply(200, dummyResponse); AjaxCache.retrieve(dummyEndpoint) - .then(unexpectedResponse) + .then(done) .catch(fail); AjaxCache.retrieve(dummyEndpoint) - .then(unexpectedResponse) + .then(done) .catch(fail); - expect($.ajax.calls.count()).toBe(1); + expect(axios.get.calls.count()).toBe(1); }); it('returns undefined if Ajax call fails and cache is empty', (done) => { - const dummyStatusText = 'exploded'; - const dummyErrorMessage = 'server exploded'; - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.reject(null, dummyStatusText, dummyErrorMessage); - return deferred.promise(); - }; + const errorMessage = 'Network Error'; + mock.onGet(dummyEndpoint).networkError(); AjaxCache.retrieve(dummyEndpoint) .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`)) .catch((error) => { - expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`); - expect(error.textStatus).toBe(dummyStatusText); + expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); + expect(error.textStatus).toBe(errorMessage); done(); }) .catch(fail); @@ -154,7 +143,9 @@ describe('AjaxCache', () => { it('makes no Ajax call if matching data exists', (done) => { AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; - ajaxSpy = () => fail(new Error('expected no Ajax call!')); + mock.onGet(dummyEndpoint).reply(() => { + fail(new Error('expected no Ajax call!')); + }); AjaxCache.retrieve(dummyEndpoint) .then((data) => { @@ -171,12 +162,7 @@ describe('AjaxCache', () => { AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse; - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; + mock.onGet(dummyEndpoint).reply(200, dummyResponse); // Call without forceRetrieve param AjaxCache.retrieve(dummyEndpoint) @@ -189,7 +175,7 @@ describe('AjaxCache', () => { // Call with forceRetrieve param AjaxCache.retrieve(dummyEndpoint, true) .then((data) => { - expect(data).toBe(dummyResponse); + expect(data).toEqual(dummyResponse); }) .then(done) .catch(fail); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 0a9d815f469..80430011aed 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,6 +1,7 @@ /* eslint-disable promise/catch-or-return */ - +import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; +import MockAdapter from 'axios-mock-adapter'; describe('common_utils', () => { describe('parseUrl', () => { @@ -413,48 +414,48 @@ describe('common_utils', () => { describe('setCiStatusFavicon', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + let mock; beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); document.body.appendChild(favicon); + mock = new MockAdapter(axios); }); afterEach(() => { + mock.restore(); document.body.removeChild(document.getElementById('favicon')); }); - it('should reset favicon in case of error', () => { - const favicon = document.getElementById('favicon'); - spyOn($, 'ajax').and.callFake(function (options) { - options.error(); - expect(favicon.getAttribute('href')).toEqual('null'); - }); + it('should reset favicon in case of error', (done) => { + mock.onGet(BUILD_URL).networkError(); - commonUtils.setCiStatusFavicon(BUILD_URL); + commonUtils.setCiStatusFavicon(BUILD_URL) + .then(() => { + const favicon = document.getElementById('favicon'); + expect(favicon.getAttribute('href')).toEqual('null'); + done(); + }) + // Error is already caught in catch() block of setCiStatusFavicon, + // It won't throw another error for us to catch + .catch(done.fail); }); - it('should set page favicon to CI status favicon based on provided status', () => { + it('should set page favicon to CI status favicon based on provided status', (done) => { const FAVICON_PATH = '//icon_status_success'; - const favicon = document.getElementById('favicon'); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ favicon: FAVICON_PATH }); - expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + mock.onGet(BUILD_URL).reply(200, { + favicon: FAVICON_PATH, }); - commonUtils.setCiStatusFavicon(BUILD_URL); - }); - }); - - describe('ajaxPost', () => { - it('should perform `$.ajax` call and do `POST` request', () => { - const requestURL = '/some/random/api'; - const data = { keyname: 'value' }; - const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); - - commonUtils.ajaxPost(requestURL, data); - expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + commonUtils.setCiStatusFavicon(BUILD_URL) + .then(() => { + const favicon = document.getElementById('favicon'); + expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + done(); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 1f46c225071..69a23d7b2f3 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -62,4 +62,14 @@ describe('text_utility', () => { expect(textUtils.slugify('João')).toEqual('joão'); }); }); + + describe('stripHtml', () => { + it('replaces html tag with the default replacement', () => { + expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + }); + + it('replaces html tags with the provided replacement', () => { + expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + }); + }); }); diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js index ec6ea35952b..50371c8c5f6 100644 --- a/spec/javascripts/lib/utils/users_cache_spec.js +++ b/spec/javascripts/lib/utils/users_cache_spec.js @@ -92,7 +92,9 @@ describe('UsersCache', () => { apiSpy = (query, options) => { expect(query).toBe(''); expect(options).toEqual({ username: dummyUsername }); - return Promise.resolve([dummyUser]); + return Promise.resolve({ + data: [dummyUser], + }); }; UsersCache.retrieve(dummyUsername) diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e983e4de3fc..5d0ee91d977 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 2f02c11482f..bdfd16ac995 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, no-return-assign */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import MergeRequest from '~/merge_request'; import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import IssuablesHelper from '~/helpers/issuables_helper'; @@ -7,11 +8,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; (function() { describe('MergeRequest', function() { describe('task lists', function() { + let mock; + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + + spyOn(axios, 'patch').and.callThrough(); + mock = new MockAdapter(axios); + + mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {}); + return this.merge = new MergeRequest(); }); + + afterEach(() => { + mock.restore(); + }); + it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); const changeEvent = document.createEvent('HTMLEvents'); @@ -19,17 +33,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); - return expect(req.data.merge_request.description).not.toBe(null); + + it('submits an ajax request on tasklist:changed', (done) => { + $('.js-task-list-field').trigger('tasklist:changed'); + + setTimeout(() => { + expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, { + merge_request: { description: '- [ ] Task List Item' }, + }); + done(); }); - return $('.js-task-list-field').trigger('tasklist:changed'); }); }); describe('class constructor', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax').and.stub(); + }); + it('calls .initCloseReopenReport', () => { spyOn(IssuablesHelper, 'initCloseReopenReport'); @@ -63,8 +84,8 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + new MergeRequest(); // eslint-disable-line no-new + MergeRequest.hideCloseButton(); }); it('hides the dropdown close item and selects the next item', () => { @@ -83,8 +104,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + MergeRequest.hideCloseButton(); }); it('hides the close button', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index a6be474805b..fda24db98b4 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,5 +1,6 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; @@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo'; describe('activateTab', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function () {}); + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); @@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo'; describe('setCurrentAction', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function () {}); + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); this.subject = this.class.setCurrentAction; }); @@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo'; }); describe('tabShown', () => { + let mock; + beforeEach(function () { - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: '' }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, { + data: { html: '' }, }); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); }); + afterEach(() => { + mock.restore(); + }); + describe('with "Side-by-side"/parallel diff view', () => { beforeEach(function () { this.class.diffViewType = () => 'parallel'; @@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo'; it('triggers Ajax request to JSON endpoint', function (done) { const url = '/foo/bar/merge_requests/1/diffs'; - spyOn(this.class, 'ajaxGet').and.callFake((options) => { - expect(options.url).toEqual(`${url}.json`); + + spyOn(axios, 'get').and.callFake((reqUrl) => { + expect(reqUrl).toBe(`${url}.json`); + done(); + + return Promise.resolve({ data: {} }); }); this.class.loadDiff(url); }); it('triggers scroll event when diff already loaded', function (done) { - spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail()); + spyOn(axios, 'get').and.callFake(done.fail); spyOn(document, 'dispatchEvent'); this.class.diffsLoaded = true; @@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo'; describe('with inline diff', () => { let noteId; let noteLineNumId; + let mock; beforeEach(() => { const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); @@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo'; .attr('href') .replace('#', ''); - spyOn($, 'ajax').and.callFake(function (options) { - options.success(diffsResponse); - }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); + }); + + afterEach(() => { + mock.restore(); }); describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function () { + it('should expand and scroll to linked fragment hash #note_xxx', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, + setTimeout(() => { + expect(noteId.length).toBeGreaterThan(0); + expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); + + done(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { + it('should gracefully ignore non-existant fragment hash', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + setTimeout(() => { + expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + + done(); + }); }); }); @@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo'; describe('with parallel diff', () => { let noteId; let noteLineNumId; + let mock; beforeEach(() => { const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); @@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo'; .attr('href') .replace('#', ''); - spyOn($, 'ajax').and.callFake(function (options) { - options.success(diffsResponse); - }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); + }); + + afterEach(() => { + mock.restore(); }); describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function () { + it('should expand and scroll to linked fragment hash #note_xxx', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'new', - forceShow: true, + setTimeout(() => { + expect(noteId.length).toBeGreaterThan(0); + expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'new', + forceShow: true, + }); + + done(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { + it('should gracefully ignore non-existant fragment hash', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + setTimeout(() => { + expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + done(); + }); }); }); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 481b46c3ac6..6fa6f44f953 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,9 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; -import '~/flash'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Mini Pipeline Graph Dropdown', () => { preloadFixtures('static/mini_dropdown_graph.html.raw'); @@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => { }); describe('When dropdown is clicked', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + it('should call getBuildsList', () => { const getBuildsListSpy = spyOn( MiniPipelineGraph.prototype, @@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => { }); it('should make a request to the endpoint provided in the html', () => { - const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + const ajaxSpy = spyOn(axios, 'get').and.callThrough(); + + mock.onGet('foobar').reply(200, { + html: '', + }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + expect(ajaxSpy.calls.allArgs()[0][0]).toEqual('foobar'); }); - it('should not close when user uses cmd/ctrl + click', () => { - spyOn($, 'ajax').and.callFake(function (params) { - params.success({ - html: `<li> - <a class="mini-pipeline-graph-dropdown-item" href="#"> - <span class="ci-status-icon ci-status-icon-failed"></span> - <span class="ci-build-text">build</span> - </a> - <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> - </li>`, - }); + it('should not close when user uses cmd/ctrl + click', (done) => { + mock.onGet('foobar').reply(200, { + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); - - expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + timeoutPromise() + .then(() => { + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + }) + .then(timeoutPromise) + .then(() => { + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }) + .then(done) + .catch(done.fail); }); - }); - it('should close the dropdown when request returns an error', (done) => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('should close the dropdown when request returns an error', (done) => { + mock.onGet('foobar').networkError(); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); + document.querySelector('.js-builds-dropdown-button').click(); - setTimeout(() => { - expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); - done(); - }, 0); + setTimeout(() => { + expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); + done(); + }); + }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 9885b8a790f..eb8f6bbe50d 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -38,7 +38,7 @@ describe('Dashboard', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('shows up a loading state', (done) => { diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index bf6ada8185e..d07db871d69 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -11,168 +11,38 @@ const createComponent = (propsData) => { }; describe('MonitoringDeployment', () => { - const reducedDeploymentData = [deploymentData[0]]; - reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name; - reducedDeploymentData[0].xPos = 10; - reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at); describe('Methods', () => { - it('refText shows the ref when a tag is available', () => { - reducedDeploymentData[0].tag = '1.0'; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toEqual(reducedDeploymentData[0].ref); - }); - - it('refText shows the sha when no tag is available', () => { - reducedDeploymentData[0].tag = null; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toContain('f5bcd1'); - }); - - it('nameDeploymentClass creates a class with the prefix deploy-info-', () => { + it('should contain a hidden gradient', () => { const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, + showDeployInfo: true, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); - expect( - component.nameDeploymentClass(reducedDeploymentData[0]), - ).toContain('deploy-info'); + expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); }); it('transformDeploymentGroup translates an available deployment', () => { const component = createComponent({ showDeployInfo: false, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); expect( - component.transformDeploymentGroup(reducedDeploymentData[0]), + component.transformDeploymentGroup({ xPos: 16 }), ).toContain('translate(11, 20)'); }); - it('hides the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); - }); - - it('positions the flag to the left when the xPos is too far right', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - reducedDeploymentData[0].xPos = 250; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.positionFlag(reducedDeploymentData[0]), - ).toBeLessThan(0); - }); - - it('shows the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelector('.js-deploy-info-box').style.display, - ).not.toEqual('display: none;'); - }); - - it('contains date, refs and the "deployed" text', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Deployed'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Wed, May 31'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText(component.refText(reducedDeploymentData[0])); - }); - - it('contains a link to the commit contents', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'), - ).not.toEqual(''); - }); - - it('should contain a hidden gradient', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); - }); - describe('Computed props', () => { it('calculatedHeight', () => { const component = createComponent({ showDeployInfo: true, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 8ee1171419d..2d474e9092f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; +import { deploymentData } from '../mock_data'; const createComponent = (propsData) => { const Component = Vue.extend(GraphFlag); @@ -9,11 +10,6 @@ const createComponent = (propsData) => { }).$mount(); }; -function getCoordinate(component, selector, coordinate) { - const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate); - return parseInt(coordinateVal, 10); -} - const defaultValuesComponent = { currentXCoordinate: 200, currentYCoordinate: 100, @@ -25,31 +21,111 @@ const defaultValuesComponent = { graphHeight: 300, graphHeightOffset: 120, showFlagContent: true, + realPixelRatio: 1, + timeSeries: [{ + values: [{ + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }], + }], + unitOfDisplay: 'ms', + currentDataIndex: 0, + legendTitle: 'Average', +}; + +const deploymentFlagData = { + ...deploymentData[0], + ref: deploymentData[0].ref.name, + xPos: 10, + time: new Date(deploymentData[0].created_at), }; describe('GraphFlag', () => { - it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { - const component = createComponent(defaultValuesComponent); + let component; - expect(getCoordinate(component, '.selected-metric-line', 'x1')) - .toEqual(component.currentXCoordinate); - expect(getCoordinate(component, '.selected-metric-line', 'x2')) - .toEqual(component.currentXCoordinate); + it('has a line at the currentXCoordinate', () => { + component = createComponent(defaultValuesComponent); + + expect(component.$el.style.left) + .toEqual(`${70 + component.currentXCoordinate}px`); }); - it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { - const component = createComponent(defaultValuesComponent); + describe('Deployment flag', () => { + it('shows a deployment flag when deployment data provided', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.popover-title'), + ).toContainText('Deployed'); + }); + + it('contains the ref when a tag is available', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: true, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('1.0'); + }); + + it('does not contain the ref when a tag is unavailable', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: false, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); - const svg = component.$el.querySelector('.rect-text-metric'); - expect(svg.tagName).toEqual('svg'); - expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition); + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).not.toContainText('1.0'); + }); }); describe('Computed props', () => { - it('calculatedHeight', () => { - const component = createComponent(defaultValuesComponent); + beforeEach(() => { + component = createComponent(defaultValuesComponent); + }); + + it('formatTime', () => { + expect(component.formatTime).toMatch(/\d:17PM/); + }); + + it('formatDate', () => { + expect(component.formatDate).toEqual('Sun, Jun 4'); + }); + + it('cursorStyle', () => { + expect(component.cursorStyle).toEqual({ + top: '20px', + left: '270px', + height: '180px', + }); + }); - expect(component.calculatedHeight).toEqual(180); + it('flagOrientation', () => { + expect(component.flagOrientation).toEqual('left'); }); }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 1f4e858e731..2bbe963e393 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1,6 +1,8 @@ /* eslint-disable quote-props, indent, comma-dangle */ -const metricsGroupsAPIResponse = { +export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; + +export const metricsGroupsAPIResponse = { 'success': true, 'data': [ { diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index 58f4fbc5791..8f8ba231ae8 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -42,6 +42,18 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); + it('sanitizes output', (done) => { + Object.assign(cell, { + source: ['[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n'], + }); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('a')).toBeNull(); + + done(); + }); + }); + describe('katex', () => { beforeEach(() => { json = getJSONFixture('blob/notebook/math.json'); diff --git a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js new file mode 100644 index 00000000000..d587573fc9e --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js @@ -0,0 +1,66 @@ +export default { + 'protocol-based JS injection: simple, no spaces': { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before': { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces after': { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: preceding colon': { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: null char': { + input: '<a href=java\0script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: invalid URL char': { + input: '<img src=java\script:alert("XSS")>', // eslint-disable-line no-useless-escape + output: '<img>', + }, + 'protocol-based JS injection: Unicode': { + input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: spaces and entities': { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'img on error': { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, +}; diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/javascripts/notebook/cells/output/html_spec.js new file mode 100644 index 00000000000..9c5385f2922 --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import htmlOutput from '~/notebook/cells/output/html.vue'; +import sanitizeTests from './html_sanitize_tests'; + +describe('html output cell', () => { + function createComponent(rawCode) { + const Component = Vue.extend(htmlOutput); + + return new Component({ + propsData: { + rawCode, + }, + }).$mount(); + } + + describe('sanitizes output', () => { + Object.keys(sanitizeTests).forEach((key) => { + it(key, () => { + const test = sanitizeTests[key]; + const vm = createComponent(test.input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + + expect(outputEl.innerHTML).toEqual(test.output); + + vm.$destroy(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 20e352dd8bd..104d03377b6 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -139,13 +139,21 @@ describe('issue_comment_form component', () => { }); describe('event enter', () => { - it('should save note when cmd/ctrl+enter is pressed', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); + + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 7c8d6685ee1..36c56cd3862 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 86e9e2a32a9..f841a408d09 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -69,13 +69,20 @@ describe('issue_note_form component', () => { }); describe('enter', () => { - it('should submit note', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleUpdate').and.callThrough(); vm.$el.querySelector('textarea').value = 'Foo'; vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleUpdate).toHaveBeenCalled(); }); + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index c8a6cb7e612..cb63b64724d 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,4 +1,4 @@ - +import _ from 'underscore'; import Vue from 'vue'; import store from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 6b608adff15..f0c800c759d 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -29,7 +29,6 @@ export const noteableDataMock = { can_create_note: true, can_update: true, }, - deleted_at: null, description: '', due_date: null, human_time_estimate: null, @@ -108,7 +107,7 @@ export const note = { "name": "Administrator", "username": "root", "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "path": "/root" }, "created_at": "2017-08-10T15:24:03.087Z", @@ -283,7 +282,6 @@ export const loggedOutnoteableData = { "updated_by_id": 1, "created_at": "2017-02-07T10:11:18.395Z", "updated_at": "2017-08-08T10:22:51.564Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 167f074fb9b..274d7591c71 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,10 +1,14 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ +import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; import '~/render_gfm'; import Notes from '~/notes'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; (function() { window.gon || (window.gon = {}); @@ -46,13 +50,24 @@ import Notes from '~/notes'; }); describe('task lists', function() { + let mock; + beforeEach(function() { + spyOn(axios, 'patch').and.callThrough(); + mock = new MockAdapter(axios); + + mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {}); + $('.js-comment-button').on('click', function(e) { e.preventDefault(); }); this.notes = new Notes('', []); }); + afterEach(() => { + mock.restore(); + }); + it('modifies the Markdown field', function() { const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); @@ -61,14 +76,15 @@ import Notes from '~/notes'; expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json'); - return expect(req.data.note).not.toBe(null); - }); + it('submits an ajax request on tasklist:changed', function(done) { + $('.js-task-list-container').trigger('tasklist:changed'); - $('.js-task-list-field.js-note-text').trigger('tasklist:changed'); + setTimeout(() => { + expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, { + note: { note: '' }, + }); + done(); + }); }); }); @@ -118,6 +134,7 @@ import Notes from '~/notes'; let noteEntity; let $form; let $notesContainer; + let mock; beforeEach(() => { this.notes = new Notes('', []); @@ -135,24 +152,32 @@ import Notes from '~/notes'; $form = $('form.js-main-target-form'); $notesContainer = $('ul.main-notes-list'); $form.find('textarea.js-note-text').val(sampleComment); + + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, noteEntity); }); - it('updates note and resets edit form', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('updates note and resets edit form', (done) => { spyOn(this.notes, 'revertNoteEditForm'); spyOn(this.notes, 'setupNewNote'); $('.js-comment-button').click(); - deferred.resolve(noteEntity); - const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); - const updatedNote = Object.assign({}, noteEntity); - updatedNote.note = 'bar'; - this.notes.updateNote(updatedNote, $targetNote); + setTimeout(() => { + const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); + const updatedNote = Object.assign({}, noteEntity); + updatedNote.note = 'bar'; + this.notes.updateNote(updatedNote, $targetNote); + + expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); + expect(this.notes.setupNewNote).toHaveBeenCalled(); - expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); - expect(this.notes.setupNewNote).toHaveBeenCalled(); + done(); + }); }); }); @@ -478,8 +503,19 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; + + function mockNotesPost() { + mock.onPost(/(.*)\/notes$/).reply(200, note); + } + + function mockNotesPostError() { + mock.onPost(/(.*)\/notes$/).networkError(); + } beforeEach(() => { + mock = new MockAdapter(axios); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -488,63 +524,92 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').val(sampleComment); }); + afterEach(() => { + mock.restore(); + }); + it('should show placeholder note while new comment is being posted', () => { + mockNotesPost(); + $('.js-comment-button').click(); expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); }); - it('should remove placeholder note when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should remove placeholder note when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($notesContainer.find('.note.being-posted').length).toEqual(0); + setTimeout(() => { + expect($notesContainer.find('.note.being-posted').length).toEqual(0); + + done(); + }); }); - it('should show actual note element when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show actual note element when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); + setTimeout(() => { + expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); + + done(); + }); }); - it('should reset Form when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should reset Form when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($form.find('textarea.js-note-text').val()).toEqual(''); + setTimeout(() => { + expect($form.find('textarea.js-note-text').val()).toEqual(''); + + done(); + }); }); - it('should show flash error message when new comment failed to be posted', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show flash error message when new comment failed to be posted', (done) => { + mockNotesPostError(); + $('.js-comment-button').click(); - deferred.reject(); - expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); + setTimeout(() => { + expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); + + done(); + }); }); - it('should show flash error message when comment failed to be updated', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show flash error message when comment failed to be updated', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - const $noteEl = $notesContainer.find(`#note_${note.id}`); - $noteEl.find('.js-note-edit').click(); - $noteEl.find('textarea.js-note-text').val(updatedComment); - $noteEl.find('.js-comment-save-button').click(); + timeoutPromise() + .then(() => { + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').val(updatedComment); - deferred.reject(); - const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`); - expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals - expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original - expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown + mock.restore(); + + mockNotesPostError(); + + $noteEl.find('.js-comment-save-button').click(); + }) + .then(timeoutPromise) + .then(() => { + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`); + expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original + expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown + + done(); + }) + .catch(done.fail); }); }); @@ -562,8 +627,12 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, note); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -581,15 +650,20 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').val(sampleComment); }); - it('should remove slash command placeholder when comment with slash commands is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('should remove slash command placeholder when comment with slash commands is done posting', (done) => { spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); $('.js-comment-button').click(); expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown - deferred.resolve(note); - expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + + setTimeout(() => { + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + done(); + }); }); }); @@ -606,8 +680,12 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, note); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -616,19 +694,24 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').html(sampleComment); }); - it('should not render a script tag', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('should not render a script tag', (done) => { $('.js-comment-button').click(); - deferred.resolve(note); - const $noteEl = $notesContainer.find(`#note_${note.id}`); - $noteEl.find('.js-note-edit').click(); - $noteEl.find('textarea.js-note-text').html(updatedComment); - $noteEl.find('.js-comment-save-button').click(); + setTimeout(() => { + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').html(updatedComment); + $noteEl.find('.js-comment-save-button').click(); + + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); - const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); - expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); + done(); + }); }); }); diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index f90e0093d25..b24563f738b 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -1,4 +1,4 @@ -import OAuthRememberMe from '~/oauth_remember_me'; +import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { preloadFixtures('static/oauth_remember_me.html.raw'); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 2fd87754238..b09494f0b77 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,5 +1,6 @@ /* global fixture */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as utils from '~/lib/utils/url_utility'; import Pager from '~/pager'; @@ -9,7 +10,6 @@ describe('pager', () => { beforeEach(() => { setFixtures('<div class="content_list"></div><div class="loading"></div>'); - spyOn($, 'ajax'); }); afterEach(() => { @@ -47,39 +47,90 @@ describe('pager', () => { }); describe('getOld', () => { + const urlRegex = /(.*)some_list(.*)$/; + let mock; + + function mockSuccess() { + mock.onGet(urlRegex).reply(200, { + count: 0, + html: '', + }); + } + + function mockError() { + mock.onGet(urlRegex).networkError(); + } + beforeEach(() => { setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + spyOn(axios, 'get').and.callThrough(); + + mock = new MockAdapter(axios); + Pager.init(); }); - it('shows loader while loading next page', () => { + afterEach(() => { + mock.restore(); + }); + + it('shows loader while loading next page', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'show'); Pager.getOld(); - expect(Pager.loading.show).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.show).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on success', () => { - spyOn($, 'ajax').and.callFake(options => options.success({})); + it('hides loader on success', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on error', () => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('hides loader on error', (done) => { + mockError(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('sends request to url with offset and limit params', () => { - spyOn($, 'ajax'); + it('sends request to url with offset and limit params', (done) => { Pager.offset = 100; Pager.limit = 20; Pager.getOld(); - const [{ data, url }] = $.ajax.calls.argsFor(0); - expect(data).toBe('limit=20&offset=100'); - expect(url).toBe('/some_list'); + + setTimeout(() => { + const [url, params] = axios.get.calls.argsFor(0); + + expect(params).toEqual({ + params: { + limit: 20, + offset: 100, + }, + }); + expect(url).toBe('/some_list'); + + done(); + }); }); }); }); diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index 7f6b5873011..d2386077aa6 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -1,5 +1,5 @@ import '~/lib/utils/text_utility'; -import AbuseReports from '~/abuse_reports'; +import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; describe('Abuse Reports', () => { const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js new file mode 100644 index 00000000000..440a6585d57 --- /dev/null +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../../helpers/vue_mount_component_helper'; + +describe('stop_jobs_modal.vue', () => { + const props = { + url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + const Component = Vue.extend(stopJobsModal); + vm = mountComponent(Component, props); + }); + + describe('onSubmit', () => { + it('stops jobs and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`; + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if stopping jobs failed', (done) => { + const dummyError = new Error('stopping jobs failed'); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.reject(dummyError); + }); + + vm.onSubmit() + .then(done.fail) + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js new file mode 100644 index 00000000000..3cd33a3e900 --- /dev/null +++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue'; +import eventHub from '~/pages/milestones/shared/event_hub'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('delete_milestone_modal.vue', () => { + const Component = Vue.extend(deleteMilestoneModal); + const props = { + issueCount: 1, + mergeRequestCount: 2, + milestoneId: 3, + milestoneTitle: 'my milestone title', + milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('onSubmit', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + spyOn(eventHub, '$emit'); + }); + + it('deletes milestone and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true }); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if deleting milestone failed', (done) => { + const dummyError = new Error('deleting milestone failed'); + dummyError.response = { status: 418 }; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.reject(dummyError); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false }); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('text', () => { + it('contains the issue and milestone count', () => { + vm = mountComponent(Component, props); + const value = vm.text; + + expect(value).toContain('remove it from 1 issue and 2 merge requests'); + }); + + it('contains neither issue nor milestone count', () => { + vm = mountComponent(Component, { ...props, + issueCount: 0, + mergeRequestCount: 0, + }); + + const value = vm.text; + + expect(value).toContain('is not currently used'); + }); + }); +}); diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js deleted file mode 100644 index 5b316b319a5..00000000000 --- a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { - setupPipelineVariableList, - insertRow, - removeRow, -} from '~/pipeline_schedules/setup_pipeline_variable_list'; - -describe('Pipeline Variable List', () => { - let $markup; - - describe('insertRow', () => { - it('should insert another row', () => { - $markup = $(`<div> - <li class="js-row"> - <input> - <textarea></textarea> - </li> - </div>`); - - insertRow($markup.find('.js-row')); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should clear `data-is-persisted` on cloned row', () => { - $markup = $(`<div> - <li class="js-row" data-is-persisted="true"></li> - </div>`); - - insertRow($markup.find('.js-row')); - - const $lastRow = $markup.find('.js-row').last(); - expect($lastRow.attr('data-is-persisted')).toBe(undefined); - }); - - it('should clear inputs on cloned row', () => { - $markup = $(`<div> - <li class="js-row"> - <input value="foo"> - <textarea>bar</textarea> - </li> - </div>`); - - insertRow($markup.find('.js-row')); - - const $lastRow = $markup.find('.js-row').last(); - expect($lastRow.find('input').val()).toBe(''); - expect($lastRow.find('textarea').val()).toBe(''); - }); - }); - - describe('removeRow', () => { - it('should remove dynamic row', () => { - $markup = $(`<div> - <li class="js-row"> - <input> - <textarea></textarea> - </li> - </div>`); - - removeRow($markup.find('.js-row')); - - expect($markup.find('.js-row').length).toBe(0); - }); - - it('should hide and mark to destroy with already persisted rows', () => { - $markup = $(`<div> - <li class="js-row" data-is-persisted="true"> - <input class="js-destroy-input"> - </li> - </div>`); - - const $row = $markup.find('.js-row'); - removeRow($row); - - expect($row.find('.js-destroy-input').val()).toBe('1'); - expect($markup.find('.js-row').length).toBe(1); - }); - }); - - describe('setupPipelineVariableList', () => { - beforeEach(() => { - $markup = $(`<form> - <li class="js-row"> - <input class="js-user-input" name="schedule[variables_attributes][][key]"> - <textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea> - <button class="js-row-remove-button"></button> - <button class="js-row-add-button"></button> - </li> - </form>`); - - setupPipelineVariableList($markup); - }); - - it('should remove the row when clicking the remove button', () => { - $markup.find('.js-row-remove-button').trigger('click'); - - expect($markup.find('.js-row').length).toBe(0); - }); - - it('should add another row when editing the last rows key input', () => { - const $row = $markup.find('.js-row'); - $row.find('input.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should add another row when editing the last rows value textarea', () => { - const $row = $markup.find('.js-row'); - $row.find('textarea.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should remove empty row after blurring', () => { - const $row = $markup.find('.js-row'); - $row.find('input.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - - $row.find('input.js-user-input') - .val('') - .trigger('input') - .trigger('blur'); - - expect($markup.find('.js-row').length).toBe(1); - }); - - it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { - const $row = $markup.find('.js-row'); - expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]'); - expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]'); - - $markup.filter('form').submit(); - - expect($row.find('input').attr('name')).toBe(''); - expect($row.find('textarea').attr('name')).toBe(''); - }); - }); -}); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 48620898357..d010d897642 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -13,7 +13,7 @@ describe('Pipelines Async Button', () => { propsData: { endpoint: '/foo', title: 'Foo', - icon: 'fa fa-foo', + icon: 'repeat', cssClass: 'bar', }, }).$mount(); @@ -23,8 +23,8 @@ describe('Pipelines Async Button', () => { expect(component.$el.tagName).toEqual('BUTTON'); }); - it('should render the provided icon', () => { - expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + it('should render svg icon', () => { + expect(component.$el.querySelector('svg')).not.toBeNull(); }); it('should render the provided title', () => { diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 6611b74594f..97f04844b3a 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,11 +24,11 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continous Integration can help catch bugs by running your tests automatically'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continuous Deployment can help you deliver code to your product environment'); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 35e36e9c353..c3dc7b53d0f 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -61,14 +61,14 @@ describe('pipeline graph job component', () => { it('it should render status and name', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4257, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4257', has_details: false, }, }, @@ -118,7 +118,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is not provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4258, name: 'test', status: { icon: 'icon_status_success', @@ -132,7 +132,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4259, name: 'test', status: { icon: 'icon_status_success', diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index 063ab53681b..f744f1af5e6 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -4,18 +4,18 @@ import stageColumnComponent from '~/pipelines/components/graph/stage_column_comp describe('stage column component', () => { let component; const mockJob = { - id: 4256, + id: 4250, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4250', action: { icon: 'retry', title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', + path: '/root/ci-mock/builds/4250/retry', method: 'post', }, }, @@ -24,10 +24,17 @@ describe('stage column component', () => { beforeEach(() => { const StageColumnComponent = Vue.extend(stageColumnComponent); + const mockJobs = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = Object.assign({}, mockJob); + mockedJob.id += i; + mockJobs.push(mockedJob); + } + component = new StageColumnComponent({ propsData: { title: 'foo', - jobs: [mockJob, mockJob, mockJob], + jobs: mockJobs, }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index f1697840fcd..09a0c14d96c 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -14,6 +14,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -31,6 +32,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: false, }; @@ -41,12 +43,31 @@ describe('Pipelines Nav Controls', () => { expect(component.$el.querySelector('.btn-create')).toEqual(null); }); + it('should render link for resetting runner caches', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + canCreatePipeline: false, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); + expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); + }); + it('should render link for CI lint', () => { const mockData = { newPipelinePath: 'foo', hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -54,8 +75,8 @@ describe('Pipelines Nav Controls', () => { propsData: mockData, }).$mount(); - expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint'); - expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath); + expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint'); + expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath); }); it('should render link to help page when CI is not enabled', () => { @@ -64,6 +85,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: false, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -81,6 +103,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index 9fec2f61f78..bc6413a159f 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 367b42cefb0..a99ebc4e51a 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index a9126d2f4e9..de744739e42 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => { beforeEach(() => { const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); - pipelineWithoutAuthor = pipelines.find(p => p.id === 2); - pipelineWithoutCommit = pipelines.find(p => p.id === 3); + + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); + pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null); + pipelineWithoutCommit = pipelines.find(p => p.user === null && p.commit === null); }); afterEach(() => { diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index ca2f9163313..4fc3c08145e 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -11,9 +11,10 @@ describe('Pipelines Table', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTableComponent = Vue.extend(pipelinesTableComp); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTableComponent = Vue.extend(pipelinesTableComp); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('table', () => { diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 1b96b2e3d51..61c2f783acc 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import stage from '~/pipelines/components/stage.vue'; diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js index 2e94948cfb2..588b61196a5 100644 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) @@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index 850768f0e4f..c314ca8ab72 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -6,8 +6,12 @@ describe('New Project', () => { beforeEach(() => { setFixtures(` - <input id="project_import_url" /> - <input id="project_path" /> + <div class='toggle-import-form'> + <div class='import-url-data'> + <input id="project_import_url" /> + <input id="project_path" /> + </div> + </div> `); $projectImportUrl = $('#project_import_url'); @@ -25,7 +29,7 @@ describe('New Project', () => { it('does not change project path for disabled $projectImportUrl', () => { $projectImportUrl.attr('disabled', true); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -38,7 +42,7 @@ describe('New Project', () => { it('does not change project path if it is set by user', () => { $projectPath.keyup(); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -46,7 +50,7 @@ describe('New Project', () => { it('does not change project path for empty $projectImportUrl', () => { $projectImportUrl.val(''); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -54,7 +58,7 @@ describe('New Project', () => { it('does not change project path for whitespace $projectImportUrl', () => { $projectImportUrl.val(' '); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -62,7 +66,7 @@ describe('New Project', () => { it('does not change project path for $projectImportUrl without slashes', () => { $projectImportUrl.val('has-no-slash'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -70,7 +74,7 @@ describe('New Project', () => { it('changes project path to last $projectImportUrl component', () => { $projectImportUrl.val('/this/is/last'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('last'); }); @@ -78,7 +82,7 @@ describe('New Project', () => { it('ignores trailing slashes in $projectImportUrl', () => { $projectImportUrl.val('/has/trailing/slash/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('slash'); }); @@ -86,7 +90,7 @@ describe('New Project', () => { it('ignores fragment identifier in $projectImportUrl', () => { $projectImportUrl.val('/this/has/a#fragment-identifier/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('a'); }); @@ -94,7 +98,7 @@ describe('New Project', () => { it('ignores query string in $projectImportUrl', () => { $projectImportUrl.val('/url/with?query=string'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('with'); }); @@ -102,7 +106,7 @@ describe('New Project', () => { it('ignores trailing .git in $projectImportUrl', () => { $projectImportUrl.val('/repository.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('repository'); }); @@ -110,7 +114,7 @@ describe('New Project', () => { it('changes project path for HTTPS URL in $projectImportUrl', () => { $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('project'); }); @@ -118,7 +122,7 @@ describe('New Project', () => { it('changes project path for SSH URL in $projectImportUrl', () => { $projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('gitlab-ce'); }); diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js index b24567ffc0c..f6c0f51cf62 100644 --- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PANEL_STATE from '~/prometheus_metrics/constants'; import { metrics, missingVarMetrics } from './mock_data'; @@ -102,25 +104,38 @@ describe('PrometheusMetrics', () => { describe('loadActiveMetrics', () => { let prometheusMetrics; + let mock; + + function mockSuccess() { + mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, { + data: metrics, + success: true, + }); + } + + function mockError() { + mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError(); + } beforeEach(() => { + spyOn(axios, 'get').and.callThrough(); + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); }); it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + mockSuccess(); prometheusMetrics.loadActiveMetrics(); expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); - expect($.ajax).toHaveBeenCalledWith({ - url: prometheusMetrics.activeMetricsEndpoint, - dataType: 'json', - global: false, - }); - - deferred.resolve({ data: metrics, success: true }); + expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); setTimeout(() => { expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); @@ -129,14 +144,10 @@ describe('PrometheusMetrics', () => { }); it('should show empty state if response failed to load', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - spyOn(prometheusMetrics, 'populateActiveMetrics'); + mockError(); prometheusMetrics.loadActiveMetrics(); - deferred.reject(); - setTimeout(() => { expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); @@ -145,14 +156,11 @@ describe('PrometheusMetrics', () => { }); it('should populate metrics list once response is loaded', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(prometheusMetrics, 'populateActiveMetrics'); + mockSuccess(); prometheusMetrics.loadActiveMetrics(); - deferred.resolve({ data: metrics, success: true }); - setTimeout(() => { expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); done(); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 43e7d9e1224..6a8a85e3dfb 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; @@ -89,7 +90,7 @@ describe('Registry List', () => { it('should render empty message', (done) => { setTimeout(() => { expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toEqual('No container images stored for this project. Add one by following the instructions above.'); done(); }, 0); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js index c4d3866c922..debde1bb357 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js @@ -12,7 +12,7 @@ describe('Multi-file editor commit sidebar list collapsed', () => { vm = createComponentWithStore(Component, store); - vm.$store.state.openFiles.push(file(), file()); + vm.$store.state.openFiles.push(file('file1'), file('file2')); vm.$store.state.openFiles[0].tempFile = true; vm.$store.state.openFiles.forEach((f) => { Object.assign(f, { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js index fc7c9ae9dd7..4b20fdf70d6 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js @@ -10,7 +10,7 @@ describe('Multi-file editor commit sidebar list item', () => { beforeEach(() => { const Component = Vue.extend(listItem); - f = file(); + f = file('test-file'); vm = mountComponent(Component, { file: f, diff --git a/spec/javascripts/repo/components/ide_repo_tree_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js index b6f70f585cd..e3bbda514da 100644 --- a/spec/javascripts/repo/components/ide_repo_tree_spec.js +++ b/spec/javascripts/repo/components/ide_repo_tree_spec.js @@ -41,11 +41,11 @@ describe('IdeRepoTree', () => { expect(tbody.querySelector('.file')).toBeTruthy(); }); - it('renders 5 loading files if tree is loading', (done) => { - vm.$store.state.loading = true; + it('renders 3 loading files if tree is loading', (done) => { + vm.treeId = '123'; Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); done(); }); diff --git a/spec/javascripts/repo/components/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js index 20b8dc25dcb..acfd63eb8de 100644 --- a/spec/javascripts/repo/components/ide_spec.js +++ b/spec/javascripts/repo/components/ide_spec.js @@ -10,7 +10,9 @@ describe('ide component', () => { beforeEach(() => { const Component = Vue.extend(ide); - vm = createComponentWithStore(Component, store).$mount(); + vm = createComponentWithStore(Component, store, { + emptyStateSvgPath: 'svg', + }).$mount(); }); afterEach(() => { diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index b001c1655b4..6efbbf6d75e 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -57,16 +57,17 @@ describe('new dropdown component', () => { }); }); - describe('toggleModalOpen', () => { + describe('hideModal', () => { + beforeAll((done) => { + vm.openModal = true; + Vue.nextTick(done); + }); + it('closes modal after toggling', (done) => { - vm.toggleModalOpen(); + vm.hideModal(); Vue.nextTick() .then(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }) - .then(vm.toggleModalOpen) - .then(() => { expect(vm.$el.querySelector('.modal')).toBeNull(); }) .then(done) diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js index 233cca06ed0..8bbc3100357 100644 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js @@ -18,8 +18,10 @@ describe('new file modal component', () => { })); spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { - id: '123branch', + data: { + commit: { + id: '123branch', + }, }, })); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js index 788c08e5279..667112ab21a 100644 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js @@ -17,8 +17,10 @@ describe('new dropdown upload', () => { })); spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { - id: '123branch', + data: { + commit: { + id: '123branch', + }, }, })); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index cd93fb3ccbf..93e94b4f24c 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -29,7 +29,7 @@ describe('RepoCommitSection', () => { comp.$store.state.rightPanelCollapsed = false; comp.$store.state.currentBranch = 'master'; - comp.$store.state.openFiles = [file(), file()]; + comp.$store.state.openFiles = [file('file1'), file('file2')]; comp.$store.state.openFiles.forEach(f => Object.assign(f, { changed: true, content: 'testing', @@ -87,8 +87,10 @@ describe('RepoCommitSection', () => { changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles)); spyOn(service, 'commit').and.returnValue(Promise.resolve({ - short_id: '1', - stats: {}, + data: { + short_id: '1', + stats: {}, + }, })); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index e8b370f97b4..27b55ed1f87 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -25,44 +25,29 @@ describe('RepoFile', () => { vm = new RepoFile({ store, propsData: { - file: file(), + file: file('t4'), }, }); spyOn(vm, 'timeFormated').and.returnValue(updated); vm.$mount(); const name = vm.$el.querySelector('.repo-file-name'); - const fileIcon = vm.$el.querySelector('.file-icon'); - expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px'); expect(name.href).toMatch(''); expect(name.textContent.trim()).toEqual(vm.file.name); - expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy(); - expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`); }); it('does render if hasFiles is true and is loading tree', () => { vm = createComponent({ - file: file(), + file: file('t1'), }); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); - it('renders a spinner if the file is loading', () => { - const f = file(); - f.loading = true; - vm = createComponent({ - file: f, - }); - - expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull(); - expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`); - }); - it('does not render commit message and datetime if mini', (done) => { vm = createComponent({ - file: file(), + file: file('t2'), }); vm.$store.state.openFiles.push(vm.file); @@ -76,7 +61,7 @@ describe('RepoFile', () => { it('fires clickFile when the link is clicked', () => { vm = createComponent({ - file: file(), + file: file('t3'), }); spyOn(vm, 'clickFile'); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index 507bca983df..933e8d3a06a 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -56,7 +56,7 @@ describe('RepoTab', () => { }); it('renders an fa-circle icon if tab is changed', () => { - const tab = file(); + const tab = file('changedFile'); tab.changed = true; vm = createComponent({ tab, @@ -68,7 +68,7 @@ describe('RepoTab', () => { describe('methods', () => { describe('closeTab', () => { it('does not close tab if is changed', (done) => { - const tab = file(); + const tab = file('closeFile'); tab.changed = true; tab.opened = true; vm = createComponent({ diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index 0beaf643793..2c363364d70 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -4,7 +4,7 @@ import repoTabs from '~/ide/components/repo_tabs.vue'; import { file, resetStore } from '../helpers'; describe('RepoTabs', () => { - const openedFiles = [file(), file()]; + const openedFiles = [file('open1'), file('open2')]; let vm; function createComponent() { diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js index 8ce01d3bf12..e2d8f002e27 100644 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -18,7 +18,7 @@ describe('Multi-file store file actions', () => { oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - localFile = file(); + localFile = file('testFile'); localFile.active = true; localFile.opened = true; localFile.parentTreeUrl = 'parentTreeUrl'; @@ -81,7 +81,7 @@ describe('Multi-file store file actions', () => { }); it('sets next file as active', (done) => { - const f = file(); + const f = file('otherfile'); store.state.openFiles.push(f); expect(f.active).toBeFalsy(); @@ -119,7 +119,7 @@ describe('Multi-file store file actions', () => { }); it('calls scrollToTab', (done) => { - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('setThisActive')) .then(() => { expect(scrollToTabSpy).toHaveBeenCalled(); @@ -128,7 +128,7 @@ describe('Multi-file store file actions', () => { }); it('sets the file active', (done) => { - const localFile = file(); + const localFile = file('activeFile'); store.dispatch('setFileActive', localFile) .then(() => { @@ -139,7 +139,7 @@ describe('Multi-file store file actions', () => { }); it('returns early if file is already active', (done) => { - const localFile = file(); + const localFile = file('earlyActive'); localFile.active = true; store.dispatch('setFileActive', localFile) @@ -151,11 +151,11 @@ describe('Multi-file store file actions', () => { }); it('sets current active file to not active', (done) => { - const localFile = file(); + const localFile = file('currentActive'); localFile.active = true; store.state.openFiles.push(localFile); - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('newActive')) .then(() => { expect(localFile.active).toBeFalsy(); @@ -166,7 +166,7 @@ describe('Multi-file store file actions', () => { it('resets location.hash for line highlighting', (done) => { location.hash = 'test'; - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('otherActive')) .then(() => { expect(location.hash).not.toBe('test'); @@ -176,7 +176,7 @@ describe('Multi-file store file actions', () => { }); describe('getFileData', () => { - let localFile = file(); + let localFile; beforeEach(() => { spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ @@ -194,10 +194,17 @@ describe('Multi-file store file actions', () => { }), })); - localFile = file(); + localFile = file('newCreate'); localFile.url = 'getFileDataURL'; }); + afterEach(() => { + store.dispatch('closeFile', { + file: localFile, + force: true, + }); + }); + it('calls the service', (done) => { store.dispatch('getFileData', localFile) .then(() => { @@ -268,7 +275,7 @@ describe('Multi-file store file actions', () => { beforeEach(() => { spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('calls getRawFileData service method', (done) => { @@ -294,7 +301,7 @@ describe('Multi-file store file actions', () => { let tmpFile; beforeEach(() => { - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('updates file content', (done) => { diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js index 0b0d34f072a..f678967b092 100644 --- a/spec/javascripts/repo/stores/actions_spec.js +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -48,14 +48,14 @@ describe('Multi-file store actions', () => { describe('discardAllChanges', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardAll')); store.state.openFiles[0].changed = true; }); }); describe('closeAllFiles', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('closeAll')); store.state.openFiles[0].opened = true; }); @@ -97,7 +97,7 @@ describe('Multi-file store actions', () => { it('opens discard popup if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardChanges')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode') @@ -111,7 +111,7 @@ describe('Multi-file store actions', () => { it('can force closed if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('forceClose')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode', true) @@ -124,7 +124,7 @@ describe('Multi-file store actions', () => { }); it('discards file changes', (done) => { - const f = file(); + const f = file('discard'); store.state.editMode = true; store.state.openFiles.push(f); f.changed = true; @@ -178,7 +178,9 @@ describe('Multi-file store actions', () => { it('calls service', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '123' }, + data: { + commit: { id: '123' }, + }, })); store.dispatch('checkCommitStatus') @@ -192,7 +194,9 @@ describe('Multi-file store actions', () => { it('returns true if current ref does not equal returned ID', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '123' }, + data: { + commit: { id: '123' }, + }, })); store.dispatch('checkCommitStatus') @@ -206,7 +210,9 @@ describe('Multi-file store actions', () => { it('returns false if current ref equals returned ID', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '1' }, + data: { + commit: { id: '1' }, + }, })); store.dispatch('checkCommitStatus') @@ -250,13 +256,15 @@ describe('Multi-file store actions', () => { describe('success', () => { beforeEach(() => { spyOn(service, 'commit').and.returnValue(Promise.resolve({ - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - stats: { - additions: '1', - deletions: '2', + data: { + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, }, })); }); @@ -285,8 +293,8 @@ describe('Multi-file store actions', () => { }); it('adds commit data to changed files', (done) => { - const changedFile = file(); - const f = file(); + const changedFile = file('changed'); + const f = file('newfile'); changedFile.changed = true; store.state.openFiles.push(changedFile, f); @@ -300,19 +308,6 @@ describe('Multi-file store actions', () => { }).catch(done.fail); }); - it('closes all files', (done) => { - store.state.openFiles.push(file()); - store.state.openFiles[0].opened = true; - - store.dispatch('commitChanges', { payload, newMr: false }) - .then(Vue.nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - it('scrolls to top of page', (done) => { store.dispatch('commitChanges', { payload, newMr: false }) .then(() => { @@ -337,7 +332,9 @@ describe('Multi-file store actions', () => { describe('failed', () => { beforeEach(() => { spyOn(service, 'commit').and.returnValue(Promise.resolve({ - message: 'failed message', + data: { + message: 'failed message', + }, })); }); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js index 947a60587df..6e204ef0404 100644 --- a/spec/javascripts/repo/stores/mutations/file_spec.js +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -117,7 +117,7 @@ describe('Multi-file store file mutations', () => { describe('CREATE_TMP_FILE', () => { it('adds file into parent tree', () => { - const f = file(); + const f = file('tmpFile'); mutations.CREATE_TMP_FILE(localState, { file: f, diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js index cf1248ba28b..e6ca8ea139e 100644 --- a/spec/javascripts/repo/stores/mutations/tree_spec.js +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -57,7 +57,7 @@ describe('Multi-file store tree mutations', () => { describe('CREATE_TMP_TREE', () => { it('adds tree into parent tree', () => { - const tmpEntry = file(); + const tmpEntry = file('tmpTree'); mutations.CREATE_TMP_TREE(localState, { tmpEntry, diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 3267e29585b..35bb630bf5d 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,6 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ +import MockAdapter from 'axios-mock-adapter'; import '~/commons/bootstrap'; +import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; (function() { @@ -35,16 +37,23 @@ import Sidebar from '~/right_sidebar'; var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); loadJSONFixtures('todos/todos.json'); + let mock; beforeEach(function() { loadFixtures(fixtureName); - this.sidebar = new Sidebar; + mock = new MockAdapter(axios); + this.sidebar = new Sidebar(); $aside = $('.right-sidebar'); $page = $('.layout-page'); $icon = $aside.find('i'); $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); }); + + afterEach(() => { + mock.restore(); + }); + it('should expand/collapse the sidebar when arrow is clicked', function() { assertSidebarState('expanded'); $toggle.click(); @@ -63,20 +72,19 @@ import Sidebar from '~/right_sidebar'; return assertSidebarState('collapsed'); }); - it('should broadcast todo:toggle event when add todo clicked', function() { + it('should broadcast todo:toggle event when add todo clicked', function(done) { var todos = getJSONFixture('todos/todos.json'); - spyOn(jQuery, 'ajax').and.callFake(function() { - var d = $.Deferred(); - var response = todos; - d.resolve(response); - return d.promise(); - }); + mock.onPost(/(.*)\/todos$/).reply(200, todos); var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); $('.issuable-sidebar-header .js-issuable-todo').click(); - expect(todoToggleSpy.calls.count()).toEqual(1); + setTimeout(() => { + expect(todoToggleSpy.calls.count()).toEqual(1); + + done(); + }); }); it('should not hide collapsed icons', () => { diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js new file mode 100644 index 00000000000..38e94d45e55 --- /dev/null +++ b/spec/javascripts/search_spec.js @@ -0,0 +1,40 @@ +import Api from '~/api'; +import Search from '~/pages/search/show/search'; + +describe('Search', () => { + const fixturePath = 'search/show.html.raw'; + const searchTerm = 'some search'; + const fillDropdownInput = (dropdownSelector) => { + const dropdownElement = document.querySelector(dropdownSelector).parentNode; + const inputElement = dropdownElement.querySelector('.dropdown-input-field'); + inputElement.value = searchTerm; + return inputElement; + }; + + preloadFixtures(fixturePath); + + beforeEach(() => { + loadFixtures(fixturePath); + new Search(); // eslint-disable-line no-new + }); + + it('requests groups from backend when filtering', (done) => { + spyOn(Api, 'groups').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-group-dropdown'); + + $(inputElement).trigger('input'); + }); + + it('requests projects from backend when filtering', (done) => { + spyOn(Api, 'projects').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-project-dropdown'); + + $(inputElement).trigger('input'); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 3b094d20838..d9e84e35f69 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -15,7 +15,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-02-02T21: 49: 49.664Z', updated_at: '2017-05-03T22: 26: 03.760Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -28,7 +27,7 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/user0', }, { @@ -36,7 +35,7 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/tajuana', }, { @@ -44,7 +43,7 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/michaele.will', }, ], @@ -73,24 +72,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], human_time_estimate: null, @@ -101,24 +100,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], subscribed: true, @@ -153,7 +152,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-06-27T19:54:42.437Z', updated_at: '2017-08-18T03:39:49.222Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -184,7 +182,7 @@ const mockData = { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, rootPath: '/', fullPath: '/gitlab-org/gitlab-shell', @@ -196,7 +194,7 @@ const mockData = { human_total_time_spent: null, }, user: { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: 1, name: 'Administrator', username: 'root', diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index b97e24d9dcf..6bb6d639f24 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 9efd109b996..afa18cc127e 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 8b0d51bbcc8..97f762d07a7 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index ea4eae1e23f..3591f96ff87 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -6,14 +6,14 @@ const ASSIGNEE = { id: 2, name: 'gitlab user 2', username: 'gitlab2', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const ANOTHER_ASSINEE = { id: 3, name: 'gitlab user 3', username: 'gitlab3', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const PARTICIPANT = { @@ -38,7 +38,7 @@ describe('Sidebar store', () => { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, editable: true, rootPath: '/', diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 9b33dd02fb9..79db05f04ed 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -20,23 +20,23 @@ describe('Subscriptions', function () { subscribed: undefined, }); - expect(vm.$refs.loadingButton.loading).toBe(true); - expect(vm.$refs.loadingButton.label).toBeUndefined(); + expect(vm.$refs.toggleButton.isLoading).toBe(true); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-loading'); }); - it('has "Subscribe" text when currently not subscribed', () => { + it('is toggled "off" when currently not subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: false, }); - expect(vm.$refs.loadingButton.label).toBe('Subscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass('is-checked'); }); - it('has "Unsubscribe" text when currently not subscribed', () => { + it('is toggled "on" when currently subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: true, }); - expect(vm.$refs.loadingButton.label).toBe('Unsubscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked'); }); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index a53e8a94d89..b1b03ef1e09 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -1,5 +1,5 @@ import AccessorUtilities from '~/lib/utils/accessor'; -import SigninTabsMemoizer from '~/signin_tabs_memoizer'; +import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; (() => { describe('SigninTabsMemoizer', () => { @@ -53,6 +53,13 @@ import SigninTabsMemoizer from '~/signin_tabs_memoizer'; expect(memo.readData()).toEqual('#standard'); }); + it('overrides last selected tab with hash tag when given', () => { + window.location.hash = '#ldap'; + createMemoizer(); + + expect(memo.readData()).toEqual('#ldap'); + }); + describe('class constructor', () => { beforeEach(() => { memo = createMemoizer(); diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 1c87fcec245..7265e1b6cb5 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import SmartInterval from '~/smart_interval'; describe('SmartInterval', function () { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 6897c991066..9b2a5379855 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,12 +1,13 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; import Vue from 'vue'; import VueResource from 'vue-resource'; +import { getDefaultAdapter } from '~/lib/utils/axios_utils'; + const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); Vue.config.devtools = !isHeadlessChrome; Vue.config.productionTip = false; @@ -31,7 +32,6 @@ jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; // globalize common libraries window.$ = window.jQuery = $; -window._ = _; // stub expected globals window.gl = window.gl || {}; @@ -61,6 +61,8 @@ beforeEach(() => { Vue.http.interceptors = builtinVueHttpInterceptors.slice(); }); +const axiosDefaultAdapter = getDefaultAdapter(); + // render all of our tests const testsContext = require.context('.', true, /_spec$/); testsContext.keys().forEach(function (path) { @@ -96,6 +98,12 @@ describe('test errors', () => { it('has no Vue error', () => { expect(hasVueErrors).toBe(false); }); + + it('restores axios adapter after mocking', () => { + if (getDefaultAdapter() !== axiosDefaultAdapter) { + fail('axios adapter is not restored! Did you forget a restore() on MockAdapter?'); + } + }); }); // if we're generating coverage reports, make sure to include all files so diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 59e16f0786e..35871dddf89 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,5 @@ import * as urlUtils from '~/lib/utils/url_utility'; -import Todos from '~/todos'; +import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; describe('Todos', () => { diff --git a/spec/javascripts/toggle_buttons_spec.js b/spec/javascripts/toggle_buttons_spec.js new file mode 100644 index 00000000000..205e396d682 --- /dev/null +++ b/spec/javascripts/toggle_buttons_spec.js @@ -0,0 +1,120 @@ +import setupToggleButtons from '~/toggle_buttons'; +import getSetTimeoutPromise from './helpers/set_timeout_promise_helper'; + +function generateMarkup(isChecked = true) { + return ` + <button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle"> + <input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" /> + </button> + `; +} + +function setupFixture(isChecked, clickCallback) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = generateMarkup(isChecked); + + setupToggleButtons(wrapper, clickCallback); + + return wrapper; +} + +describe('ToggleButtons', () => { + describe('when input value is true', () => { + it('should initialize as checked', () => { + const wrapper = setupFixture(true); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }); + + it('should toggle to unchecked when clicked', (done) => { + const wrapper = setupFixture(true); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when input value is false', () => { + it('should initialize as unchecked', () => { + const wrapper = setupFixture(false); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }); + + it('should toggle to checked when clicked', (done) => { + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('should emit `trigger-change` event', (done) => { + const changeSpy = jasmine.createSpy('changeEventHandler'); + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + const input = wrapper.querySelector('.js-project-feature-toggle-input'); + + $(input).on('trigger-change', changeSpy); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(changeSpy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('clickCallback', () => { + it('should show loading indicator while waiting', (done) => { + const isChecked = true; + const clickCallback = (newValue, toggleButton) => { + const input = toggleButton.querySelector('.js-project-feature-toggle-input'); + + expect(newValue).toEqual(false); + + // Check for the loading state + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(true); + expect(toggleButton.disabled).toEqual(true); + expect(input.value).toEqual('true'); + + // After the callback finishes, check that the loading state is gone + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(false); + expect(toggleButton.disabled).toEqual(false); + expect(input.value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }; + + const wrapper = setupFixture(isChecked, clickCallback); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js index a750bc78f36..f14d5f6f76c 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js @@ -1,39 +1,39 @@ import Vue from 'vue'; -import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author'; - -const author = { - webUrl: 'http://foo.bar', - avatarUrl: 'http://gravatar.com/foo', - name: 'fatihacet', -}; -const createComponent = () => { - const Component = Vue.extend(authorComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { author }, - }); -}; +import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetAuthor', () => { - describe('props', () => { - it('should have props', () => { - const authorProp = authorComponent.props.author; + let vm; + + beforeEach(() => { + const Component = Vue.extend(authorComponent); + + vm = mountComponent(Component, { + author: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, - expect(authorProp).toBeDefined(); - expect(authorProp.type instanceof Object).toBeTruthy(); - expect(authorProp.required).toBeTruthy(); }); }); - describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; + afterEach(() => { + vm.$destroy(); + }); - expect(el.tagName).toEqual('A'); - expect(el.getAttribute('href')).toEqual(author.webUrl); - expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl); - expect(el.querySelector('.author').innerText.trim()).toEqual(author.name); - }); + it('renders link with the author web url', () => { + expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root'); + }); + + it('renders image with avatar url', () => { + expect( + vm.$el.querySelector('img').getAttribute('src'), + ).toEqual('http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon'); + }); + + it('renders author name', () => { + expect(vm.$el.textContent.trim()).toEqual('Administrator'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js index 515ddcbb875..8c55622b15e 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -1,61 +1,40 @@ import Vue from 'vue'; -import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time'; - -const props = { - actionText: 'Merged by', - author: { - webUrl: 'http://foo.bar', - avatarUrl: 'http://gravatar.com/foo', - name: 'fatihacet', - }, - dateTitle: '2017-03-23T23:02:00.807Z', - dateReadable: '12 hours ago', -}; -const createComponent = () => { - const Component = Vue.extend(authorTimeComponent); - - return new Component({ - el: document.createElement('div'), - propsData: props, - }); -}; +import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetAuthorTime', () => { - describe('props', () => { - it('should have props', () => { - const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props; - const ActionTextClass = actionText.type; - const DateTitleClass = dateTitle.type; - const DateReadableClass = dateReadable.type; - - expect(new ActionTextClass() instanceof String).toBeTruthy(); - expect(actionText.required).toBeTruthy(); - - expect(author.type instanceof Object).toBeTruthy(); - expect(author.required).toBeTruthy(); - - expect(new DateTitleClass() instanceof String).toBeTruthy(); - expect(dateTitle.required).toBeTruthy(); - - expect(new DateReadableClass() instanceof String).toBeTruthy(); - expect(dateReadable.required).toBeTruthy(); + let vm; + + beforeEach(() => { + const Component = Vue.extend(authorTimeComponent); + + vm = mountComponent(Component, { + actionText: 'Merged by', + author: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + dateTitle: '2017-03-23T23:02:00.807Z', + dateReadable: '12 hours ago', }); }); - describe('components', () => { - it('should have components', () => { - expect(authorTimeComponent.components['mr-widget-author']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided action text', () => { + expect(vm.$el.textContent).toContain('Merged by'); }); - describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; + it('renders author', () => { + expect(vm.$el.textContent).toContain('Administrator'); + }); - expect(el.tagName).toEqual('H4'); - expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl); - expect(el.querySelector('time').innerText).toContain(props.dateReadable); - expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle); - }); + it('renders provided time', () => { + expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z'); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index db7d083065b..6a59dc3c87e 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -95,10 +95,8 @@ describe('MRWidgetDeployment', () => { const url = '/foo/bar'; const returnPromise = () => new Promise((resolve) => { resolve({ - json() { - return { - redirect_url: url, - }; + data: { + redirect_url: url, }, }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 06f89fabf42..13e5595bbfc 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -1,104 +1,219 @@ import Vue from 'vue'; -import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header'; - -const createComponent = (mr) => { - const Component = Vue.extend(headerComponent); - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetHeader', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = headerComponent.props; + let vm; + let Component; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(headerComponent); + }); + + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { - let vm; - beforeEach(() => { - vm = createComponent({ - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '/foo/bar/mr-widget-refactor', - targetBranch: 'master', + describe('shouldShowCommitsBehindText', () => { + it('return true when there are divergedCommitsCount', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); + + expect(vm.shouldShowCommitsBehindText).toEqual(true); + }); + + it('returns false where there are no divergedComits count', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); + expect(vm.shouldShowCommitsBehindText).toEqual(false); }); }); - it('shouldShowCommitsBehindText', () => { - expect(vm.shouldShowCommitsBehindText).toBeTruthy(); + describe('commitsText', () => { + it('returns singular when there is one commit', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 1, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); - vm.mr.divergedCommitsCount = 0; - expect(vm.shouldShowCommitsBehindText).toBeFalsy(); - }); + expect(vm.commitsText).toEqual('1 commit behind'); + }); - it('commitsText', () => { - expect(vm.commitsText).toEqual('commits'); + it('returns plural when there is more than one commit', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 2, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); - vm.mr.divergedCommitsCount = 1; - expect(vm.commitsText).toEqual('commit'); + expect(vm.commitsText).toEqual('2 commits behind'); + }); }); }); describe('template', () => { - let vm; - let el; - const sourceBranchPath = '/foo/bar/mr-widget-refactor'; - const mr = { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - }; - - beforeEach(() => { - vm = createComponent(mr); - el = vm.$el; + describe('common elements', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders source branch link', () => { + expect( + vm.$el.querySelector('.js-source-branch').innerHTML, + ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>'); + }); + + it('renders clipboard button', () => { + expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null); + }); + + it('renders target branch', () => { + expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master'); + }); + }); + + describe('with an open merge request', () => { + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders checkout branch button with modal trigger', () => { + const button = vm.$el.querySelector('.js-check-out-branch'); + + expect(button.textContent.trim()).toEqual('Check out branch'); + expect(button.getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(button.getAttribute('data-toggle')).toEqual('modal'); + }); + + it('renders download dropdown with links', () => { + expect( + vm.$el.querySelector('.js-download-email-patches').textContent.trim(), + ).toEqual('Email patches'); + + expect( + vm.$el.querySelector('.js-download-email-patches').getAttribute('href'), + ).toEqual('/mr/email-patches'); + + expect( + vm.$el.querySelector('.js-download-plain-diff').textContent.trim(), + ).toEqual('Plain diff'); + + expect( + vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'), + ).toEqual('/mr/plainDiffPath'); + }); }); - it('should render template elements correctly', () => { - expect(el.classList.contains('mr-source-target')).toBeTruthy(); - const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; - const targetBranchLink = el.querySelectorAll('.label-branch')[1]; - const commitsCount = el.querySelector('.diverged-commits-count'); - - expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); - expect(targetBranchLink.textContent).toContain(mr.targetBranch); - expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); - expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath); - expect(commitsCount.textContent).toContain('12 commits behind'); - expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); - - expect(el.textContent).toContain('Check out branch'); - expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath); - expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath); + describe('with a closed merge request', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: false, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('does not render checkout branch button with modal trigger', () => { + const button = vm.$el.querySelector('.js-check-out-branch'); + + expect(button).toEqual(null); + }); + + it('does not render download dropdown with links', () => { + expect( + vm.$el.querySelector('.js-download-email-patches'), + ).toEqual(null); + + expect( + vm.$el.querySelector('.js-download-plain-diff'), + ).toEqual(null); + }); }); - it('should not have right action links if the MR state is not open', (done) => { - vm.mr.isOpen = false; - Vue.nextTick(() => { - expect(el.textContent).not.toContain('Check out branch'); - expect(el.querySelectorAll('.dropdown li a').length).toEqual(0); - done(); + describe('without diverged commits', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('does not render diverged commits info', () => { + expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null); }); }); - it('should not render diverged commits count if the MR has no diverged commits', (done) => { - vm.mr.divergedCommitsCount = null; - Vue.nextTick(() => { - expect(el.textContent).not.toContain('commits behind'); - expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0); - done(); + describe('with diverged commits', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders diverged commits info', () => { + expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index 2ae3adc1f93..07ed7f7f532 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -155,9 +155,7 @@ describe('MemoryUsage', () => { describe('loadMetrics', () => { const returnServicePromise = () => new Promise((resolve) => { resolve({ - json() { - return metricsMockData; - }, + data: metricsMockData, }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js index 4da4fc82c26..cc43639f576 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js @@ -1,51 +1,56 @@ import Vue from 'vue'; -import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help'; - -const props = { - missingBranch: 'this-is-not-the-branch-you-are-looking-for', -}; -const text = `If the ${props.missingBranch} branch exists in your local repository`; - -const createComponent = () => { - const Component = Vue.extend(mergeHelpComponent); - return new Component({ - el: document.createElement('div'), - propsData: props, - }); -}; +import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetMergeHelp', () => { - describe('props', () => { - it('should have props', () => { - const { missingBranch } = mergeHelpComponent.props; - const MissingBranchTypeClass = missingBranch.type; - - expect(new MissingBranchTypeClass() instanceof String).toBeTruthy(); - expect(missingBranch.required).toBeFalsy(); - expect(missingBranch.default).toEqual(''); - }); + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(mergeHelpComponent); }); - describe('template', () => { - let vm; - let el; + afterEach(() => { + vm.$destroy(); + }); + describe('with missing branch', () => { beforeEach(() => { - vm = createComponent(); - el = vm.$el; + vm = mountComponent(Component, { + missingBranch: 'this-is-not-the-branch-you-are-looking-for', + }); }); - it('should have the correct elements', () => { - expect(el.classList.contains('mr-widget-help')).toBeTruthy(); - expect(el.textContent).toContain(text); + it('renders missing branch information', () => { + expect( + vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '), + ).toEqual( + 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line', + ); }); - it('should not show missing branch name if missingBranch props is not provided', (done) => { - vm.missingBranch = null; - Vue.nextTick(() => { - expect(el.textContent).not.toContain(text); - done(); - }); + it('renders button to open help modal', () => { + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal'); + }); + }); + + describe('without missing branch', () => { + beforeEach(() => { + vm = mountComponent(Component); + }); + + it('renders information about how to merge manually', () => { + expect( + vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '), + ).toEqual( + 'You can merge this merge request manually using the command line', + ); + }); + + it('renders element to open a modal', () => { + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js new file mode 100644 index 00000000000..66ecaa316c8 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Merge request widget rebase component', () => { + let Component; + let vm; + beforeEach(() => { + Component = Vue.extend(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('While rebasing', () => { + it('should show progress message', () => { + vm = mountComponent(Component, { + mr: { rebaseInProgress: true }, + service: {}, + }); + + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Rebase in progress'); + }); + }); + + describe('With permissions', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: {}, + }); + }); + + it('it should render rebase button and warning message', () => { + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto the target branch or merge target'); + expect(text).toContain('branch into source branch to allow this merge request to be merged.'); + }); + + it('it should render error message when it fails', (done) => { + vm.rebasingError = 'Something went wrong!'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Something went wrong!'); + done(); + }); + }); + }); + + describe('Without permissions', () => { + it('should render a message explaining user does not have permissions', () => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: 'foo', + }, + service: {}, + }); + + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto'); + expect(text).toContain('foo'); + expect(text).toContain('to allow this merge request to be merged.'); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', (done) => { + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { + mr: {}, + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + merge_error: null, + }, + }); + }, + }, + }); + + vm.rebase(); + + // Wait for the rebase request + vm.$nextTick() + // Wait for the polling request + .then(vm.$nextTick()) + // Wait for the eventHub to be called + .then(vm.$nextTick()) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js index f86fb6a0b4b..637bf483deb 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -1,117 +1,82 @@ import Vue from 'vue'; -import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links'; +import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; -const createComponent = (data) => { - const Component = Vue.extend(relatedLinksComponent); +describe('MRWidgetRelatedLinks', () => { + let vm; - return new Component({ - el: document.createElement('div'), - propsData: data, - }); -}; + const createComponent = (data) => { + const Component = Vue.extend(relatedLinksComponent); -describe('MRWidgetRelatedLinks', () => { - describe('props', () => { - it('should have props', () => { - const { relatedLinks } = relatedLinksComponent.props; + return mountComponent(Component, data); + }; - expect(relatedLinks).toBeDefined(); - expect(relatedLinks.type instanceof Object).toBeTruthy(); - expect(relatedLinks.required).toBeTruthy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { - const data = { - relatedLinks: { - closing: '/foo', - mentioned: '/foo', - assignToMe: '/foo', - }, - }; - - describe('hasLinks', () => { - it('should return correct value when we have links reference', () => { - const vm = createComponent(data); - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.closing = null; - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.mentioned = null; - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.assignToMe = null; - expect(vm.hasLinks).toBeFalsy(); - }); - }); - describe('closesText', () => { - it('returns correct text for open merge request', () => { - data.state = 'open'; - const vm = createComponent(data); + it('returns Closes text for open merge request', () => { + vm = createComponent({ state: 'open', relatedLinks: {} }); expect(vm.closesText).toEqual('Closes'); }); it('returns correct text for closed merge request', () => { - data.state = 'closed'; - const vm = createComponent(data); + vm = createComponent({ state: 'closed', relatedLinks: {} }); expect(vm.closesText).toEqual('Did not close'); }); it('returns correct tense for merged request', () => { - data.state = 'merged'; - const vm = createComponent(data); + vm = createComponent({ state: 'merged', relatedLinks: {} }); expect(vm.closesText).toEqual('Closed'); }); }); }); - describe('template', () => { - it('should have only have closing issues text', () => { - const vm = createComponent({ - relatedLinks: { - closing: '<a href="#">#23</a> and <a>#42</a>', - }, - }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - - expect(content).toContain('Closes #23 and #42'); - expect(content).not.toContain('Mentions'); + it('should have only have closing issues text', () => { + vm = createComponent({ + relatedLinks: { + closing: '<a href="#">#23</a> and <a>#42</a>', + }, }); + const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - it('should have only have mentioned issues text', () => { - const vm = createComponent({ - relatedLinks: { - mentioned: '<a href="#">#7</a>', - }, - }); + expect(content).toContain('Closes #23 and #42'); + expect(content).not.toContain('Mentions'); + }); - expect(vm.$el.innerText).toContain('Mentions #7'); - expect(vm.$el.innerText).not.toContain('Closes'); + it('should have only have mentioned issues text', () => { + vm = createComponent({ + relatedLinks: { + mentioned: '<a href="#">#7</a>', + }, }); - it('should have closing and mentioned issues at the same time', () => { - const vm = createComponent({ - relatedLinks: { - closing: '<a href="#">#7</a>', - mentioned: '<a href="#">#23</a> and <a>#42</a>', - }, - }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); + expect(vm.$el.innerText).toContain('Mentions #7'); + expect(vm.$el.innerText).not.toContain('Closes'); + }); - expect(content).toContain('Closes #7'); - expect(content).toContain('Mentions #23 and #42'); + it('should have closing and mentioned issues at the same time', () => { + vm = createComponent({ + relatedLinks: { + closing: '<a href="#">#7</a>', + mentioned: '<a href="#">#23</a> and <a>#42</a>', + }, }); + const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - it('should have assing issues link', () => { - const vm = createComponent({ - relatedLinks: { - assignToMe: '<a href="#">Assign yourself to these issues</a>', - }, - }); + expect(content).toContain('Closes #7'); + expect(content).toContain('Mentions #23 and #42'); + }); - expect(vm.$el.innerText).toContain('Assign yourself to these issues'); + it('should have assing issues link', () => { + vm = createComponent({ + relatedLinks: { + assignToMe: '<a href="#">Assign yourself to these issues</a>', + }, }); + + expect(vm.$el.innerText).toContain('Assign yourself to these issues'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js new file mode 100644 index 00000000000..c39fcda0071 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('MR widget status icon component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(mrStatusIcon); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('while loading', () => { + it('renders loading icon', () => { + vm = mountComponent(Component, { status: 'loading' }); + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + }); + + describe('with status icon', () => { + it('renders ci status icon', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull(); + }); + }); + + describe('with disabled button', () => { + it('renders a disabled button', () => { + vm = mountComponent(Component, { status: 'failed', showDisabledButton: true }); + expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge'); + }); + }); + + describe('without disabled button', () => { + it('does not render a disabled button', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js index 4869fb17d96..f98ebdb38e6 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -1,18 +1,31 @@ import Vue from 'vue'; -import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived'; +import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetArchived', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(archivedComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('This project is archived, write access has been disabled'); - }); + beforeEach(() => { + const Component = Vue.extend(archivedComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a ci status failed icon', () => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull(); + }); + + it('renders a disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge'); + }); + + it('renders information', () => { + expect( + vm.$el.querySelector('.bold').textContent.trim(), + ).toEqual('This project is archived, write access has been disabled'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index 6042d7384d5..95c94e95e3a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -1,32 +1,47 @@ import Vue from 'vue'; -import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed'; - -const mergeError = 'This is the merge error'; +import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetAutoMergeFailed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = autoMergeFailedComponent.props.mr; + let vm; + const mergeError = 'This is the merge error'; - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); + beforeEach(() => { + const Component = Vue.extend(autoMergeFailedComponent); + vm = mountComponent(Component, { + mr: { mergeError }, }); }); - describe('template', () => { - const Component = Vue.extend(autoMergeFailedComponent); - const vm = new Component({ - el: document.createElement('div'), - propsData: { - mr: { mergeError }, - }, - }); + afterEach(() => { + vm.$destroy(); + }); + + it('renders failed message', () => { + expect(vm.$el.textContent).toContain('This merge request failed to be merged automatically'); + }); + + it('renders merge error provided', () => { + expect(vm.$el.innerText).toContain(mergeError); + }); + + it('render refresh button', () => { + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Refresh'); + }); + + it('emits event and shows loading icon when button is clicked', (done) => { + spyOn(eventHub, '$emit'); + vm.$el.querySelector('button').click(); + + expect(eventHub.$emit.calls.argsFor(0)[0]).toEqual('MRWidgetUpdateRequested'); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy(); - expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically'); - expect(vm.$el.innerText).toContain(mergeError); + Vue.nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect( + vm.$el.querySelector('button i').classList, + ).toContain('fa-spinner'); + done(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 6b7aa935ad3..658cadddb81 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -1,19 +1,29 @@ import Vue from 'vue'; -import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking'; +import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetChecking', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(checkingComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let Component; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('Checking ability to merge automatically'); - expect(el.querySelector('i')).toBeDefined(); - }); + beforeEach(() => { + Component = Vue.extend(checkingComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + + it('renders information about merging', () => { + expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index d23b558f4ea..51a34739ee9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -1,71 +1,58 @@ import Vue from 'vue'; -import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed'; - -const mr = { - targetBranch: 'good-branch', - targetBranchPath: '/good-branch', - closedEvent: { - author: { - name: 'Fatih Acet', - username: 'fatihacet', - }, - updatedAt: 'closedEventUpdatedAt', - formattedUpdatedAt: '', - }, - updatedAt: 'mrUpdatedAt', - closedAt: '1 day ago', -}; - -const createComponent = () => { - const Component = Vue.extend(closedComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetClosed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = closedComponent.props.mr; - - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + let vm; + + beforeEach(() => { + const Component = Vue.extend(closedComponent); + vm = mountComponent(Component, { mr: { + metrics: { + mergedBy: {}, + closedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + readableClosedAt: 'less than a minute ago', + }, + targetBranchPath: '/twitter/flight/commits/so_long_jquery', + targetBranch: 'so_long_jquery', + } }); }); - describe('components', () => { - it('should have components added', () => { - expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); - describe('template', () => { - let vm; - let el; + it('renders warning icon', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders closed by information with author and time', () => { + expect( + vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain( + 'Closed by Administrator less than a minute ago', + ); + }); - afterEach(() => { - vm.$destroy(); - }); + it('links to the user that closed the MR', () => { + expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root'); + }); - it('should have correct elements', () => { - expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name); - expect(el.textContent).toContain('The changes were not merged into'); - expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); - }); + it('renders information about the changes not being merged', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain('The changes were not merged into so_long_jquery'); + }); - it('should use closedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('closedEventUpdatedAt'); - }); + it('renders link for target branch', () => { + expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 5d4c7ec09dc..a7d69fdcdb9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,105 +1,85 @@ import Vue from 'vue'; -import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import mountComponent from '../../../helpers/vue_mount_component_helper'; -const ConflictsComponent = Vue.extend(conflictsComponent); -const path = '/conflicts'; - describe('MRWidgetConflicts', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = conflictsComponent.props; + let Component; + let vm; + const path = '/conflicts'; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(conflictsComponent); }); - describe('template', () => { - describe('when allowed to merge', () => { - let vm; - - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.$el.textContent).toContain('There are merge conflicts'); - expect(vm.$el.textContent).not.toContain('ask someone with write access'); - }); - - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + afterEach(() => { + vm.$destroy(); + }); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); + describe('when allowed to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, }); + }); - it('should have merge buttons', () => { - const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); - const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeButton.disabled).toBeTruthy(); - expect(mergeButton.classList.contains('btn-success')).toEqual(true); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); }); - describe('when user does not have permission to merge', () => { - let vm; + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: false, - }, - }); - }); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); - afterEach(() => { - vm.$destroy(); - }); + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - it('should show proper message', () => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - }); + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); + }); - it('should not have action buttons', () => { - expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); - expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + describe('when user does not have permission to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: false, + }, }); }); - describe('when fast-forward or semi-linear merge enabled', () => { - let vm; + it('should show proper message', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access'); + }); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - shouldBeRebased: true, - }, - }); - }); + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + }); + }); - afterEach(() => { - vm.$destroy(); + describe('when fast-forward or semi-linear merge enabled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + shouldBeRebased: true, + }, }); + }); - it('should tell you to rebase locally', () => { - expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); - expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); - }); + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index cef365eec8a..a57b9811e08 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -1,45 +1,37 @@ import Vue from 'vue'; -import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge'; +import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const mr = { - mergeError: 'Merge error happened.', -}; -const createComponent = () => { - const Component = Vue.extend(failedToMergeComponent); - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetFailedToMerge', () => { - describe('data', () => { - it('should have default data', () => { - const data = failedToMergeComponent.data(); + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(failedToMergeComponent); + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { mr: { + mergeError: 'Merge error happened.', + } }); + }); - expect(data.timer).toEqual(10); - expect(data.isRefreshing).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('timerText', () => { it('should return correct timer text', () => { - const vm = createComponent(); - expect(vm.timerText).toEqual('10 seconds'); + expect(vm.timerText).toEqual('Refreshing in 10 seconds to show the updated status...'); vm.timer = 1; - expect(vm.timerText).toEqual('a second'); + expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...'); }); }); }); describe('created', () => { it('should disable polling', () => { - spyOn(eventHub, '$emit'); - createComponent(); - expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling'); }); }); @@ -47,13 +39,10 @@ describe('MRWidgetFailedToMerge', () => { describe('methods', () => { describe('refresh', () => { it('should emit event to request component refresh', () => { - spyOn(eventHub, '$emit'); - const vm = createComponent(); - - expect(vm.isRefreshing).toBeFalsy(); + expect(vm.isRefreshing).toEqual(false); vm.refresh(); - expect(vm.isRefreshing).toBeTruthy(); + expect(vm.isRefreshing).toEqual(true); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling'); }); @@ -61,12 +50,11 @@ describe('MRWidgetFailedToMerge', () => { describe('updateTimer', () => { it('should update timer and emit event when timer end', () => { - const vm = createComponent(); spyOn(vm, 'refresh'); expect(vm.timer).toEqual(10); - for (let i = 0; i < 10; i++) { // eslint-disable-line + for (let i = 0; i < 10; i += 1) { expect(vm.timer).toEqual(10 - i); vm.updateTimer(); } @@ -76,47 +64,54 @@ describe('MRWidgetFailedToMerge', () => { }); }); - describe('template', () => { - let vm; - let el; + describe('while it is refreshing', () => { + it('renders Refresing now', (done) => { + vm.isRefreshing = true; - beforeEach(() => { - vm = createComponent(); - el = vm.$el; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual('Refreshing now'); + done(); + }); }); + }); - it('should have correct elements', (done) => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('Merge error happened.'); - expect(el.innerText).toContain('Refreshing in 10 seconds'); - expect(el.innerText).not.toContain('Merge failed.'); - expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(el.querySelector('button').innerText).toContain('Merge'); - expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now'); - expect(el.querySelector('.js-refresh-label')).toEqual(null); - expect(el.innerText).not.toContain('Refreshing now'); - setTimeout(() => { - expect(el.innerText).toContain('Refreshing in 9 seconds'); - done(); - }, 1010); + describe('while it is not regresing', () => { + it('renders warning icon and disabled merge button', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders given error', () => { + expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual('Merge error happened..'); }); - it('should just generic merge failed message if merge_error is not available', (done) => { - vm.mr.mergeError = null; + it('renders refresh button', () => { + expect(vm.$el.querySelector('.js-refresh-button').textContent.trim()).toEqual('Refresh now'); + }); - Vue.nextTick(() => { - expect(el.innerText).toContain('Merge failed.'); - expect(el.innerText).not.toContain('Merge error happened.'); - done(); - }); + it('renders remaining time', () => { + expect( + vm.$el.querySelector('.has-custom-error').textContent.trim(), + ).toEqual('Refreshing in 10 seconds to show the updated status...'); + }); + }); + + it('should just generic merge failed message if merge_error is not available', (done) => { + vm.mr.mergeError = null; + + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('Merge failed.'); + expect(vm.$el.innerText).not.toContain('Merge error happened.'); + done(); }); + }); - it('should show refresh label when refresh requested', () => { - vm.refresh(); - Vue.nextTick(() => { - expect(el.innerText).not.toContain('Merge failed. Refreshing'); - expect(el.innerText).toContain('Refreshing now'); - }); + it('should show refresh label when refresh requested', (done) => { + vm.refresh(); + Vue.nextTick(() => { + expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing'); + expect(vm.$el.innerText).toContain('Refreshing now'); + done(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js deleted file mode 100644 index 237035648cf..00000000000 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging'; - -describe('MRWidgetMerging', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = mergingComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); - }); - - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(mergingComponent); - const mr = { - targetBranchPath: '/branch-path', - targetBranch: 'branch', - }; - const el = new Component({ - el: document.createElement('div'), - propsData: { mr }, - }).$el; - - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('This merge request is in the process of being merged'); - expect(el.innerText).toContain('changes will be merged into'); - expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js index 9a71d0b47d7..df56c4e2c5c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -1,77 +1,50 @@ import Vue from 'vue'; -import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds'; +import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const targetBranchPath = '/foo/bar'; -const targetBranch = 'foo'; -const sha = '1EA2EZ34'; - -const createComponent = () => { - const Component = Vue.extend(mwpsComponent); - const mr = { - shouldRemoveSourceBranch: false, - canRemoveSourceBranch: true, - canCancelAutomaticMerge: true, - mergeUserId: 1, - currentUserId: 1, - setToMWPSBy: {}, - sha, - targetBranchPath, - targetBranch, - }; - - const service = { - cancelAutomaticMerge() {}, - mergeResource: { - save() {}, - }, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMergeWhenPipelineSucceeds', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = mwpsComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); + let vm; + const targetBranchPath = '/foo/bar'; + const targetBranch = 'foo'; + const sha = '1EA2EZ34'; + + beforeEach(() => { + const Component = Vue.extend(mwpsComponent); + spyOn(eventHub, '$emit'); + + vm = mountComponent(Component, { + mr: { + shouldRemoveSourceBranch: false, + canRemoveSourceBranch: true, + canCancelAutomaticMerge: true, + mergeUserId: 1, + currentUserId: 1, + setToMWPSBy: {}, + sha, + targetBranchPath, + targetBranch, + }, + service: { + cancelAutomaticMerge() {}, + mergeResource: { + save() {}, + }, + }, }); }); - describe('components', () => { - it('should have components added', () => { - expect(mwpsComponent.components['mr-widget-author']).toBeDefined(); - }); - }); - - describe('data', () => { - it('should have default data', () => { - const data = mwpsComponent.data(); - - expect(data.isCancellingAutoMerge).toBeFalsy(); - expect(data.isRemovingSourceBranch).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('canRemoveSourceBranch', () => { it('should return true when user is able to remove source branch', () => { - const vm = createComponent(); - expect(vm.canRemoveSourceBranch).toBeTruthy(); }); it('should return false when user id is not the same with who set the MWPS', () => { - const vm = createComponent(); - vm.mr.mergeUserId = 2; expect(vm.canRemoveSourceBranch).toBeFalsy(); @@ -83,15 +56,11 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); it('should return false when shouldRemoveSourceBranch set to false', () => { - const vm = createComponent(); - vm.mr.shouldRemoveSourceBranch = true; expect(vm.canRemoveSourceBranch).toBeFalsy(); }); it('should return false if user is not able to remove the source branch', () => { - const vm = createComponent(); - vm.mr.canRemoveSourceBranch = false; expect(vm.canRemoveSourceBranch).toBeFalsy(); }); @@ -101,16 +70,12 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { describe('methods', () => { describe('cancelAutomaticMerge', () => { it('should set flag and call service then tell main component to update the widget with data', (done) => { - const vm = createComponent(); const mrObj = { is_new_mr_data: true, }; - spyOn(eventHub, '$emit'); spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => { resolve({ - json() { - return mrObj; - }, + data: mrObj, }); })); @@ -125,14 +90,10 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { describe('removeSourceBranch', () => { it('should set flag and call service then request main component to update the widget', (done) => { - const vm = createComponent(); - spyOn(eventHub, '$emit'); spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => { resolve({ - json() { - return { - status: 'merge_when_pipeline_succeeds', - }; + data: { + status: 'merge_when_pipeline_succeeds', }, }); })); @@ -152,31 +113,23 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); describe('template', () => { - let vm; - let el; - - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds'); - expect(el.innerText).toContain('The changes will be merged into'); - expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch will not be removed'); - expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); - expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); - expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); - expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); + expect(vm.$el.innerText).toContain('The changes will be merged into'); + expect(vm.$el.innerText).toContain(targetBranch); + expect(vm.$el.innerText).toContain('The source branch will not be removed'); + expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); + expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); + expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); }); it('should disable cancel auto merge button when the action is in progress', (done) => { vm.isCancellingAutoMerge = true; Vue.nextTick(() => { - expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); done(); }); }); @@ -185,7 +138,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.mr.shouldRemoveSourceBranch = true; Vue.nextTick(() => { - const normalizedText = el.innerText.replace(/\s+/g, ' '); + const normalizedText = vm.$el.innerText.replace(/\s+/g, ' '); expect(normalizedText).toContain('The source branch will be removed'); expect(normalizedText).not.toContain('The source branch will not be removed'); done(); @@ -196,7 +149,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.mr.currentUserId = 4; Vue.nextTick(() => { - expect(el.querySelector('.js-remove-source-branch')).toEqual(null); + expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null); done(); }); }); @@ -205,7 +158,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.isRemovingSourceBranch = true; Vue.nextTick(() => { - expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy(); done(); }); }); 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 2714e8294fa..43a989393ba 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 @@ -1,105 +1,99 @@ import Vue from 'vue'; -import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged'; +import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; -const targetBranch = 'foo'; - -const createComponent = () => { - const Component = Vue.extend(mergedComponent); - const mr = { - isRemovingSourceBranch: false, - cherryPickInForkPath: false, - canCherryPickInCurrentMR: true, - revertInForkPath: false, - canRevertInCurrentMR: true, - canRemoveSourceBranch: true, - sourceBranchRemoved: true, - mergedEvent: { - author: {}, +describe('MRWidgetMerged', () => { + let vm; + const targetBranch = 'foo'; + + beforeEach(() => { + const Component = Vue.extend(mergedComponent); + const mr = { + isRemovingSourceBranch: false, + cherryPickInForkPath: false, + canCherryPickInCurrentMR: true, + revertInForkPath: false, + canRevertInCurrentMR: true, + canRemoveSourceBranch: true, + sourceBranchRemoved: true, + metrics: { + mergedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + closedBy: {}, + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableClosedAt: '', + }, updatedAt: 'mergedUpdatedAt', - formattedUpdatedAt: '', - }, - updatedAt: 'mrUpdatedAt', - targetBranch, - }; - - const service = { - removeSourceBranch() {}, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, - }); -}; + targetBranch, + }; -describe('MRWidgetMerged', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = mergedComponent.props; + const service = { + removeSourceBranch() {}, + }; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); + spyOn(eventHub, '$emit'); - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); - }); - }); - - describe('components', () => { - it('should have components added', () => { - expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + vm = mountComponent(Component, { mr, service }); }); - describe('data', () => { - it('should have default data', () => { - const data = mergedComponent.data(); - - expect(data.isMakingRequest).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('shouldShowRemoveSourceBranch', () => { - it('should correct value when fields changed', () => { - const vm = createComponent(); + it('returns true when sourceBranchRemoved is false', () => { vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowRemoveSourceBranch).toBeTruthy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(true); + }); + it('returns false wehn sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when canRemoveSourceBranch is false', () => { vm.mr.sourceBranchRemoved = false; vm.mr.canRemoveSourceBranch = false; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when is making request', () => { vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns true when all are true', () => { vm.mr.isRemovingSourceBranch = true; vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); }); }); + describe('shouldShowSourceBranchRemoving', () => { it('should correct value when fields changed', () => { - const vm = createComponent(); vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowSourceBranchRemoving).toBeFalsy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(false); vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); vm.mr.sourceBranchRemoved = false; vm.isMakingRequest = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); vm.isMakingRequest = false; vm.mr.isRemovingSourceBranch = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); }); }); }); @@ -107,14 +101,10 @@ describe('MRWidgetMerged', () => { describe('methods', () => { describe('removeSourceBranch', () => { it('should set flag and call service then request main component to update the widget', (done) => { - const vm = createComponent(); - spyOn(eventHub, '$emit'); spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => { resolve({ - json() { - return { - message: 'Branch was removed', - }; + data: { + message: 'Branch was removed', }, }); })); @@ -122,7 +112,7 @@ describe('MRWidgetMerged', () => { vm.removeSourceBranch(); setTimeout(() => { const args = eventHub.$emit.calls.argsFor(0); - expect(vm.isMakingRequest).toBeTruthy(); + expect(vm.isMakingRequest).toEqual(true); expect(args[0]).toEqual('MRWidgetUpdateRequested'); expect(args[1]).not.toThrow(); done(); @@ -131,53 +121,50 @@ describe('MRWidgetMerged', () => { }); }); - describe('template', () => { - let vm; - let el; + it('has merged by information', () => { + expect(vm.$el.textContent).toContain('Merged by'); + expect(vm.$el.textContent).toContain('Administrator'); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders branch information', () => { + expect(vm.$el.textContent).toContain('The changes were merged into'); + expect(vm.$el.textContent).toContain(targetBranch); + }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('.js-mr-widget-author')).toBeDefined(); - expect(el.innerText).toContain('The changes were merged into'); - expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch has been removed'); - expect(el.innerText).toContain('Revert'); - expect(el.innerText).toContain('Cherry-pick'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch is being removed'); - }); + it('renders information about branch being removed', () => { + expect(vm.$el.textContent).toContain('The source branch has been removed'); + }); - it('should not show source branch removed text', (done) => { - vm.mr.sourceBranchRemoved = false; + it('shows revert and cherry-pick buttons', () => { + expect(vm.$el.textContent).toContain('Revert'); + expect(vm.$el.textContent).toContain('Cherry-pick'); + }); - Vue.nextTick(() => { - expect(el.innerText).toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + it('should not show source branch removed text', (done) => { + vm.mr.sourceBranchRemoved = false; + + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should show source branch removing text', (done) => { - vm.mr.isRemovingSourceBranch = true; - vm.mr.sourceBranchRemoved = false; + it('should show source branch removing text', (done) => { + vm.mr.isRemovingSourceBranch = true; + vm.mr.sourceBranchRemoved = false; - Vue.nextTick(() => { - expect(el.innerText).toContain('The source branch is being removed'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('The source branch is being removed'); + expect(vm.$el.innerText).not.toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should use mergedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('mergedUpdatedAt'); - }); + it('should use mergedEvent mergedAt as tooltip title', () => { + expect( + vm.$el.querySelector('time').getAttribute('title'), + ).toBe('Jan 24, 2018 1:02pm GMT+0000'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js new file mode 100644 index 00000000000..0b2ed2d4086 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('MRWidgetMerging', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(mergingComponent); + + vm = mountComponent(Component, { mr: { + targetBranchPath: '/branch-path', + targetBranch: 'branch', + } }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders information about merge request being merged', () => { + expect( + vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toContain('This merge request is in the process of being merged'); + }); + + it('renders branch information', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toEqual('The changes will be merged into branch'); + expect( + vm.$el.querySelector('a').getAttribute('href'), + ).toEqual('/branch-path'); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index df3d29ee1f9..073f26cc78f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -170,14 +170,14 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.iconClass).toEqual('success'); }); - it('shows x for failed status', () => { + it('shows warning icon for failed status', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); - it('shows x for merge not allowed', () => { + it('shows warning icon for merge not allowed', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); }); @@ -292,8 +292,8 @@ describe('MRWidgetReadyToMerge', () => { describe('handleMergeButtonClick', () => { const returnPromise = status => new Promise((resolve) => { resolve({ - json() { - return { status }; + data: { + status, }, }); }); @@ -364,12 +364,17 @@ describe('MRWidgetReadyToMerge', () => { describe('handleMergePolling', () => { const returnPromise = state => new Promise((resolve) => { resolve({ - json() { - return { state, source_branch_exists: true }; + data: { + state, + source_branch_exists: true, }, }); }); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + }); + it('should call start and stop polling when MR merged', (done) => { spyOn(eventHub, '$emit'); spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); @@ -391,6 +396,47 @@ describe('MRWidgetReadyToMerge', () => { }, 333); }); + it('updates status box', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + const statusBox = document.querySelector('.status-box'); + expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); + expect(statusBox.textContent).toContain('Merged'); + + done(); + }); + }); + + it('hides close button', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy(); + + done(); + }); + }); + + it('updates merge request count badge', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); + + done(); + }); + }); + it('should continue polling until MR is merged', (done) => { spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state')); spyOn(vm, 'initiateRemoveSourceBranchPolling'); @@ -422,8 +468,8 @@ describe('MRWidgetReadyToMerge', () => { describe('handleRemoveBranchPolling', () => { const returnPromise = state => new Promise((resolve) => { resolve({ - json() { - return { source_branch_exists: state }; + data: { + source_branch_exists: state, }, }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js index 2cb3aaa6951..98ab61a0367 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -50,9 +50,7 @@ describe('MRWidgetWIP', () => { spyOn(eventHub, '$emit'); spyOn(vm.service, 'removeWIP').and.returnValue(new Promise((resolve) => { resolve({ - json() { - return mrObj; - }, + data: mrObj, }); })); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 1ad7c2d8efa..3dd75307484 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -14,7 +14,6 @@ export default { "updated_by_id": null, "created_at": "2017-04-07T12:27:26.718Z", "updated_at": "2017-04-07T15:39:25.852Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, @@ -33,24 +32,25 @@ export default { "source_project_id": 19, "target_branch": "master", "target_project_id": 19, - "merge_event": { - "author": { + "metrics": { + "merged_by": { "name": "Administrator", "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, - "updated_at": "2017-04-07T15:39:25.696Z" + "merged_at": "2017-04-07T15:39:25.696Z", + "closed_by": null, + "closed_at": null }, - "closed_event": null, "author": { "name": "Administrator", "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "merge_user": null, @@ -64,7 +64,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "active": false, @@ -159,10 +159,10 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, - "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" }, 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 74b343c573e..cd00d0a39a3 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -8,10 +8,7 @@ import mountComponent from '../helpers/vue_mount_component_helper'; const returnPromise = data => new Promise((resolve) => { resolve({ - json() { - return data; - }, - body: data, + data, }); }); diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js new file mode 100644 index 00000000000..08e4e1f8337 --- /dev/null +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('clipboard button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(clipboardButton); + vm = mountComponent(Component, { + text: 'copy me', + title: 'Copy this value into Clipboard!', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a button for clipboard', () => { + expect(vm.$el.tagName).toEqual('BUTTON'); + expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); + expect(vm.$el.querySelector('i').className).toEqual('fa fa-clipboard'); + }); + + it('should have a tooltip with default values', () => { + expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + expect(vm.$el.getAttribute('data-container')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index d5754aaa9e7..fdead874209 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -10,7 +10,7 @@ describe('Commit component', () => { CommitComponent = Vue.extend(commitComp); }); - it('should render a code-fork icon if it does not represent a tag', () => { + it('should render a fork icon if it does not represent a tag', () => { component = new CommitComponent({ propsData: { tag: false, @@ -30,7 +30,7 @@ describe('Commit component', () => { }, }).$mount(); - expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork'); + expect(component.$el.querySelector('.icon-container').children).toContain('svg'); }); describe('Given all the props', () => { diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js new file mode 100644 index 00000000000..a6a12614e77 --- /dev/null +++ b/spec/javascripts/vue_shared/components/confirmation_input_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import confirmationInput from '~/vue_shared/components/confirmation_input.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Confirmation input component', () => { + const Component = Vue.extend(confirmationInput); + const props = { + inputId: 'dummy-id', + confirmationKey: 'confirmation-key', + confirmationValue: 'confirmation-value', + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('sets id of the input field to inputId', () => { + expect(vm.$refs.enteredValue.id).toBe(props.inputId); + }); + + it('sets name of the input field to confirmationKey', () => { + expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey); + }); + }); + + describe('computed', () => { + describe('inputLabel', () => { + it('escapes confirmationValue by default', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + + it('does not escape confirmationValue if escapeValue is false', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + }); + }); + + describe('methods', () => { + describe('hasCorrectValue', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('returns false if entered value is incorrect', () => { + vm.$refs.enteredValue.value = 'incorrect'; + expect(vm.hasCorrectValue()).toBe(false); + }); + + it('returns true if entered value is correct', () => { + vm.$refs.enteredValue.value = props.confirmationValue; + expect(vm.hasCorrectValue()).toBe(true); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a33ab689dd1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import expandButton from '~/vue_shared/components/expand_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('expand button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(expandButton); + vm = mountComponent(Component, { + slots: { + expanded: '<p>Expanded!</p>', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a collpased button', () => { + expect(vm.$el.textContent.trim()).toEqual('...'); + }); + + it('hides expander on click', (done) => { + vm.$el.querySelector('button').click(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js new file mode 100644 index 00000000000..d99b17bdc79 --- /dev/null +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import fileIcon from '~/vue_shared/components/file_icon.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('File Icon component', () => { + let vm; + let FileIcon; + + beforeEach(() => { + FileIcon = Vue.extend(fileIcon); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a span element with an svg', () => { + vm = mountComponent(FileIcon, { + fileName: 'test.js', + }); + + expect(vm.$el.tagName).toEqual('SPAN'); + expect(vm.$el.querySelector('span > svg')).toBeDefined(); + }); + + it('should render a javascript icon based on file ending', () => { + vm = mountComponent(FileIcon, { + fileName: 'test.js', + }); + + expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#javascript`); + }); + + it('should render a image icon based on file ending', () => { + vm = mountComponent(FileIcon, { + fileName: 'test.png', + }); + + expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#image`); + }); + + it('should render a webpack icon based on file namer', () => { + vm = mountComponent(FileIcon, { + fileName: 'webpack.js', + }); + + expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#webpack`); + }); + + it('should render a standard folder icon', () => { + vm = mountComponent(FileIcon, { + fileName: 'js', + folder: true, + }); + + expect(vm.$el.querySelector('span > svg > use').getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#folder`); + }); + + it('should render a loading icon', () => { + vm = mountComponent(FileIcon, { + fileName: 'test.js', + loading: true, + }); + + expect( + vm.$el.querySelector('i').getAttribute('class'), + ).toEqual('fa fa-spin fa-spinner fa-1x'); + }); + + it('should add a special class and a size class', () => { + vm = mountComponent(FileIcon, { + fileName: 'test.js', + cssClasses: 'extraclasses', + size: 120, + }); + + const classList = vm.$el.firstChild.classList; + const containsSizeClass = classList.contains('s120'); + const containsCustomClass = classList.contains('extraclasses'); + expect(containsSizeClass).toBe(true); + expect(containsCustomClass).toBe(true); + }); +}); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index b4553acb341..b378a0bd896 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import headerCi from '~/vue_shared/components/header_ci_component.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Header CI Component', () => { let HeaderCi; @@ -8,7 +9,6 @@ describe('Header CI Component', () => { beforeEach(() => { HeaderCi = Vue.extend(headerCi); - props = { status: { group: 'failed', @@ -45,54 +45,65 @@ describe('Header CI Component', () => { ], hasSidebarButton: true, }; - - vm = new HeaderCi({ - propsData: props, - }).$mount(); }); afterEach(() => { vm.$destroy(); }); - it('should render status badge', () => { - expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); - expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); - expect( - vm.$el.querySelector('.ci-failed').getAttribute('href'), - ).toEqual(props.status.details_path); - }); + describe('render', () => { + beforeEach(() => { + vm = mountComponent(HeaderCi, props); + }); - it('should render item name and id', () => { - expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); - }); + it('should render status badge', () => { + expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); + expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); + expect( + vm.$el.querySelector('.ci-failed').getAttribute('href'), + ).toEqual(props.status.details_path); + }); - it('should render timeago date', () => { - expect(vm.$el.querySelector('time')).toBeDefined(); - }); + it('should render item name and id', () => { + expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); + }); - it('should render user icon and name', () => { - expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); - }); + it('should render timeago date', () => { + expect(vm.$el.querySelector('time')).toBeDefined(); + }); - it('should render provided actions', () => { - expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); - expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); - expect(vm.$el.querySelector('.link').tagName).toEqual('A'); - expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); - expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); - }); + it('should render user icon and name', () => { + expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + }); + + it('should render provided actions', () => { + expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); + expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); + expect(vm.$el.querySelector('.link').tagName).toEqual('A'); + expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); + expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); + }); - it('should show loading icon', (done) => { - vm.actions[0].isLoading = true; + it('should show loading icon', (done) => { + vm.actions[0].isLoading = true; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); - done(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); + done(); + }); + }); + + it('should render sidebar toggle button', () => { + expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); }); }); - it('should render sidebar toggle button', () => { - expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); + describe('shouldRenderTriggeredLabel', () => { + it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => { + vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false }); + + expect(vm.$el.textContent).toContain('created'); + expect(vm.$el.textContent).not.toContain('triggered'); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js index 1baf3537741..5cd3466f501 100644 --- a/spec/javascripts/vue_shared/components/loading_icon_spec.js +++ b/spec/javascripts/vue_shared/components/loading_icon_spec.js @@ -16,7 +16,8 @@ describe('Loading Icon Component', () => { ).toEqual('fa fa-spin fa-spinner fa-1x'); expect(component.$el.tagName).toEqual('DIV'); - expect(component.$el.classList.contains('text-center')).toEqual(true); + expect(component.$el.classList).toContain('text-center'); + expect(component.$el.classList).toContain('loading-container'); }); it('should render accessibility attributes', () => { diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 24209be83fe..5f980bbf36c 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -12,14 +12,14 @@ describe('Markdown field component', () => { beforeEach((done) => { vm = new Vue({ + components: { + fieldComponent, + }, data() { return { text: 'testing\n123', }; }, - components: { - fieldComponent, - }, template: ` <field-component markdown-preview-path="/preview" diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 721f4044659..a5f9c75be4e 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -2,11 +2,65 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; +const modalComponent = Vue.extend(modal); + describe('Modal', () => { - it('does not render a primary button if no primaryButtonLabel', () => { - const modalComponent = Vue.extend(modal); - const vm = mountComponent(modalComponent); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('without primaryButtonLabel', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + primaryButtonLabel: null, + }); + }); + + it('does not render a primary button', () => { + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); + }); + + describe('with id', () => { + describe('does not render a primary button', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + id: 'my-modal', + }); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull(); + }); + + it('does not show the modal immediately', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show'); + }); + + it('does not show a backdrop', () => { + expect(vm.$el.querySelector('modal-backdrop')).toBeNull(); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> + <div id="modal-container"></div> + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + const modalElement = vm.$el.querySelector('#my-modal'); + $(modalElement).on('shown.bs.modal', () => done()); - expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + modalButton.click(); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js new file mode 100644 index 00000000000..70ce3dffaba --- /dev/null +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import panelResizer from '~/vue_shared/components/panel_resizer.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Panel Resizer component', () => { + let vm; + let PanelResizer; + + const triggerEvent = (eventName, el = vm.$el, clientX = 0) => { + const event = document.createEvent('MouseEvents'); + event.initMouseEvent(eventName, true, true, window, 1, clientX, 0, clientX, 0, false, false, + false, false, 0, null); + + el.dispatchEvent(event); + }; + + beforeEach(() => { + PanelResizer = Vue.extend(panelResizer); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a div element with the correct classes and styles', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'left', + }); + + expect(vm.$el.tagName).toEqual('DIV'); + expect(vm.$el.getAttribute('class')).toBe('dragHandle dragleft'); + expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); + }); + + it('should render a div element with the correct classes for a right side panel', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'right', + }); + + expect(vm.$el.tagName).toEqual('DIV'); + expect(vm.$el.getAttribute('class')).toBe('dragHandle dragright'); + }); + + it('drag the resizer', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'left', + }); + + spyOn(vm, '$emit'); + triggerEvent('mousedown', vm.$el); + triggerEvent('mousemove', document); + triggerEvent('mouseup', document); + expect(vm.$emit.calls.allArgs()).toEqual([['resize-start', 100], ['update:size', 100], ['resize-end', 100]]); + expect(vm.size).toBe(100); + }); +}); 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 20363e78094..2de108da2ac 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 @@ -21,22 +21,21 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('toggleCollapse events', () => { - const toggleCollapse = jasmine.createSpy(); - + describe('toggleCollapse events', () => { 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(toggleCollapse).toHaveBeenCalled(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js new file mode 100644 index 00000000000..6940b04573e --- /dev/null +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -0,0 +1,77 @@ +import Vue from 'vue'; + +import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (config) => { + const Component = Vue.extend(stackedProgressBarComponent); + const defaultConfig = Object.assign({}, { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 10, + failureCount: 5, + totalCount: 20, + }, config); + + return mountComponent(Component, defaultConfig); +}; + +describe('StackedProgressBarComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('neutralCount', () => { + it('returns neutralCount based on totalCount, successCount and failureCount', () => { + expect(vm.neutralCount).toBe(5); // 20 - 10 - 5 + }); + }); + }); + + describe('methods', () => { + describe('getPercent', () => { + it('returns percentage from provided count based on `totalCount`', () => { + expect(vm.getPercent(10)).toBe(50); + }); + }); + + describe('barStyle', () => { + it('returns style string based on percentage provided', () => { + expect(vm.barStyle(50)).toBe('width: 50%;'); + }); + }); + + describe('getTooltip', () => { + it('returns label string based on label and count provided', () => { + expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10'); + }); + }); + }); + + describe('template', () => { + it('renders container element', () => { + expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy(); + }); + + it('renders empty state when count is unavailable', () => { + const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 }); + expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0); + vmX.$destroy(); + }); + + it('renders bar elements when count is available', () => { + expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index b0b78e34e0f..c63f15e5880 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -19,6 +19,22 @@ describe('Pagination component', () => { }); describe('render', () => { + it('should not render anything', () => { + component = mountComponet({ + pageInfo: { + nextPage: 1, + page: 1, + perPage: 20, + previousPage: null, + total: 15, + totalPages: 1, + }, + change: spy, + }); + + expect(component.$el.childNodes.length).toEqual(0); + }); + describe('prev button', () => { it('should be disabled and non clickable', () => { component = mountComponet({ @@ -56,7 +72,6 @@ describe('Pagination component', () => { }); component.$el.querySelector('.js-previous-button a').click(); - expect(spy).toHaveBeenCalledWith(1); }); }); 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 8450ad9dbcb..adf80d0c2bb 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 @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 45a0bb0650f..8edba1f47a3 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index b68301a066a..5100f5737c2 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -194,6 +194,12 @@ describe Backup::Manager do ) end + it 'prints the list of available backups' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('1451606400_2016_01_01_1.2.3\n 1451520000_2015_12_31')) + end + it 'fails the operation and prints an error' do expect { subject.unpack }.to raise_error SystemExit expect(progress).to have_received(:puts) diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb new file mode 100644 index 00000000000..a1cb0c07b06 --- /dev/null +++ b/spec/lib/banzai/color_parser_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Banzai::ColorParser do + describe '.parse' do + context 'HEX format' do + [ + '#abc', '#ABC', + '#d2d2d2', '#D2D2D2', + '#123a', '#123A', + '#123456aa', '#123456AA' + ].each do |color| + it "parses the valid hex color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + '#', '#1', '#12', '#12g', '#12G', + '#12345', '#r2r2r2', '#R2R2R2', '#1234567', + '# 123', '# 1234', '# 123456', '# 12345678', + '#1 2 3', '#123 4', '#12 34 56', '#123456 78' + ].each do |color| + it "does not parse the invalid hex color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + + context 'RGB format' do + [ + 'rgb(0,0,0)', 'rgb(255,255,255)', + 'rgb(0, 0, 0)', 'RGB(0,0,0)', + 'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)', + 'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)', + 'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)', + 'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)', + 'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)' + ].each do |color| + it "parses the valid rgb color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + 'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR', + 'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)', + 'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)', + 'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)', + 'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)', + 'rgb(0,0,0%)', 'rgb(101%,0%,0%)' + ].each do |color| + it "does not parse the invalid rgb color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + + context 'HSL format' do + [ + 'hsl(0,0%,0%)', 'hsl(0,100%,100%)', + 'hsl(540,0%,0%)', 'hsl(-720,0%,0%)', + 'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)', + 'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)', + 'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)', + 'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)', + 'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)', + 'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)', + 'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)', + 'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)' + ].each do |color| + it "parses the valid hsl color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + 'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)', + 'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)', + 'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)', + 'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)', + 'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)', + 'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)' + ].each do |color| + it "does not parse the invalid hsl color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + end +end diff --git a/spec/lib/banzai/filter/color_filter_spec.rb b/spec/lib/banzai/filter/color_filter_spec.rb new file mode 100644 index 00000000000..a098b037510 --- /dev/null +++ b/spec/lib/banzai/filter/color_filter_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Banzai::Filter::ColorFilter, lib: true do + include FilterSpecHelper + + let(:color) { '#F00' } + let(:color_chip_selector) { 'code > span.gfm-color_chip > span' } + + ['#123', '#1234', '#123456', '#12345678', + 'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)', + 'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color| + it "inserts color chip for supported color format #{color}" do + content = code_tag(color) + doc = filter(content) + color_chip = doc.at_css(color_chip_selector) + + expect(color_chip.content).to be_empty + expect(color_chip.parent[:class]).to eq 'gfm-color_chip' + expect(color_chip[:style]).to eq "background-color: #{color};" + end + end + + it 'ignores valid color code without backticks(code tags)' do + doc = filter(color) + + expect(doc.css('span.gfm-color_chip').size).to be_zero + end + + it 'ignores valid color code with prepended space' do + content = code_tag(' ' + color) + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores valid color code with appended space' do + content = code_tag(color + ' ') + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores valid color code surrounded by spaces' do + content = code_tag(' ' + color + ' ') + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores invalid color code' do + invalid_color = '#BAR' + content = code_tag(invalid_color) + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + def code_tag(string) + "<code>#{string}</code>" + end +end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index 935146c17fc..a41a28a56f1 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -53,7 +53,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do doc = reference_filter("See (#{reference}.)") exp = Regexp.escape(range.reference_link_text) - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)}) end it 'ignores invalid commit IDs' do @@ -222,7 +222,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do doc = reference_filter("Fixed (#{reference}.)") exp = Regexp.escape(range.reference_link_text(project)) - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)}) end it 'ignores invalid commit IDs on the referenced project' do diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 080a5f57da9..35f8792ff35 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -42,7 +42,7 @@ describe Banzai::Filter::CommitReferenceFilter do it 'links with adjacent text' do doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{commit.short_id}</a>\.\)}) end it 'ignores invalid commit IDs' do @@ -199,12 +199,12 @@ describe Banzai::Filter::CommitReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{commit.reference_link_text(project)}</a>\.\)}) end it 'ignores invalid commit IDs on the referenced project' do act = "Committed #{invalidate_reference(reference)}" - expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) + expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) end end end diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a0d391d981c..d9018a7e4fe 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -49,7 +49,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do it 'links with adjacent text' do doc = filter("Issue (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)}) end it 'includes a title attribute' do diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index 51920869545..c84b98eb225 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -14,7 +14,7 @@ describe Banzai::Filter::ImageLinkFilter do it 'does not wrap a duplicate link' do doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)) - expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/ + expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$} end it 'works with external images' do @@ -24,6 +24,6 @@ describe Banzai::Filter::ImageLinkFilter do it 'works with inline images' do doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) - expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/ + expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$} end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 3a5f52ea23f..905fbb9434b 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -288,7 +288,7 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)</a>\.\)}) end it 'includes default classes' do @@ -317,7 +317,7 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference_link}.)") - expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)}) end it 'includes default classes' do @@ -346,7 +346,7 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference_link}.)") - expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)}) end it 'includes default classes' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 158844e25ae..eeb82822f68 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -42,7 +42,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)}) end it 'ignores invalid merge IDs' do @@ -211,7 +211,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)</a>\.\)}) end end diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb index 532d25e121d..f6474c8936d 100644 --- a/spec/lib/banzai/filter/mermaid_filter_spec.rb +++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Banzai::Filter::MermaidFilter do include FilterSpecHelper - it 'adds `js-render-mermaid` class to the `pre` tag' do + it 'adds `js-render-mermaid` class to the `code` tag' do doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A-->B;\n</code></pre>") - result = doc.xpath('descendant-or-self::pre').first + result = doc.css('code').first expect(result[:class]).to include('js-render-mermaid') end diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 68643effb66..5a7858e77f3 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -46,7 +46,7 @@ describe Banzai::Filter::RedactorFilter do it 'allows permitted Project references' do user = create(:user) project = create(:project) - project.team << [user, :master] + project.add_master(user) link = reference_link(project: project.id, reference_type: 'test') doc = filter(link, current_user: user) @@ -94,7 +94,7 @@ describe Banzai::Filter::RedactorFilter do it 'removes references for project members with guest role' do member = create(:user) project = create(:project, :public) - project.team << [member, :guest] + project.add_guest(member) issue = create(:issue, :confidential, project: project) link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') @@ -128,7 +128,7 @@ describe Banzai::Filter::RedactorFilter do it 'allows references for project members' do member = create(:user) project = create(:project, :public) - project.team << [member, :developer] + project.add_developer(member) issue = create(:issue, :confidential, project: project) link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 08beede62db..3ca4652f7cc 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -5,9 +5,11 @@ describe Banzai::Filter::RelativeLinkFilter do contexts.reverse_merge!({ commit: commit, project: project, + group: group, project_wiki: project_wiki, ref: ref, - requested_path: requested_path + requested_path: requested_path, + only_path: only_path }) described_class.call(doc, contexts) @@ -25,12 +27,18 @@ describe Banzai::Filter::RelativeLinkFilter do %(<a href="#{path}">#{path}</a>) end + def nested(element) + %(<div>#{element}</div>) + end + let(:project) { create(:project, :repository) } + let(:group) { nil } let(:project_path) { project.full_path } let(:ref) { 'markdown' } let(:commit) { project.commit(ref) } let(:project_wiki) { nil } let(:requested_path) { '/' } + let(:only_path) { true } shared_examples :preserve_unchanged do it 'does not modify any relative URL in anchor' do @@ -70,6 +78,11 @@ describe Banzai::Filter::RelativeLinkFilter do expect { filter(act) }.not_to raise_error end + it 'does not raise an exception with a garbled path' do + act = link("open(/var/tmp/):%20/location%0Afrom:%20/test") + expect { filter(act) }.not_to raise_error + end + it 'ignores ref if commit is passed' do doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) expect(doc.at_css('a')['href']) @@ -223,4 +236,100 @@ describe Banzai::Filter::RelativeLinkFilter do let(:commit) { nil } # force filter to use ref instead of commit include_examples :valid_repository end + + context 'with a /upload/ URL' do + # not needed + let(:commit) { nil } + let(:ref) { nil } + let(:requested_path) { nil } + let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' } + let(:relative_path) { "/#{project.full_path}#{upload_path}" } + + context 'to a project upload' do + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(link(upload_path)) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + + it 'rebuilds relative URL for a link' do + doc = filter(link(upload_path)) + expect(doc.at_css('a')['href']).to eq(relative_path) + + doc = filter(nested(link(upload_path))) + expect(doc.at_css('a')['href']).to eq(relative_path) + end + + it 'rebuilds relative URL for an image' do + doc = filter(image(upload_path)) + expect(doc.at_css('img')['src']).to eq(relative_path) + + doc = filter(nested(image(upload_path))) + expect(doc.at_css('img')['src']).to eq(relative_path) + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports unescaped Unicode filenames' do + path = '/uploads/한글.png' + doc = filter(link(path)) + + expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png") + end + + it 'supports escaped Unicode filenames' do + path = '/uploads/한글.png' + escaped = Addressable::URI.escape(path) + doc = filter(image(escaped)) + + expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png") + end + end + + context 'to a group upload' do + let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + let(:group) { create(:group) } + let(:project) { nil } + let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + + it 'rewrites the link correctly' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(relative_path) + end + + it 'rewrites the link correctly for subgroup' do + group.update!(parent: create(:group)) + + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(relative_path) + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + end + end end diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 3a07a6dc179..e068e02d4fc 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -28,7 +28,7 @@ describe Banzai::Filter::SnippetReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Snippet (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)}) end it 'ignores invalid snippet IDs' do @@ -192,13 +192,13 @@ describe Banzai::Filter::SnippetReferenceFilter do it 'links with adjacent text' do doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(snippet.to_reference(project))}</a>\.\)}) end it 'ignores invalid snippet IDs on the referenced project' do act = "See #{invalidate_reference(reference)}" - expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) + expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) end end diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb deleted file mode 100644 index 76bc0c36ab7..00000000000 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ /dev/null @@ -1,133 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::UploadLinkFilter do - def filter(doc, contexts = {}) - contexts.reverse_merge!({ - project: project - }) - - raw_filter(doc, contexts) - end - - def raw_filter(doc, contexts = {}) - described_class.call(doc, contexts) - end - - def image(path) - %(<img src="#{path}" />) - end - - def link(path) - %(<a href="#{path}">#{path}</a>) - end - - def nested_image(path) - %(<div><img src="#{path}" /></div>) - end - - def nested_link(path) - %(<div><a href="#{path}">#{path}</a></div>) - end - - let(:project) { create(:project) } - - shared_examples :preserve_unchanged do - it 'does not modify any relative URL in anchor' do - doc = filter(link('README.md')) - expect(doc.at_css('a')['href']).to eq 'README.md' - end - - it 'does not modify any relative URL in image' do - doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' - end - end - - it 'does not raise an exception on invalid URIs' do - act = link("://foo") - expect { filter(act) }.not_to raise_error - end - - context 'with a valid repository' do - it 'rebuilds relative URL for a link' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']) - .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - - doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']) - .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'rebuilds relative URL for an image' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('img')['src']) - .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - - doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('img')['src']) - .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com')) - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - - it 'supports Unicode filenames' do - path = '/uploads/한글.png' - escaped = Addressable::URI.escape(path) - - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class) - .to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class) - .to receive(:image?).with(path).and_return(true) - - doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png" - end - end - - context 'in group context' do - let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } - let(:group) { create(:group) } - let(:filter_context) { { project: nil, group: group } } - let(:relative_path) { "groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } - - it 'rewrites the link correctly' do - doc = raw_filter(upload_link, filter_context) - - expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}") - end - - it 'rewrites the link correctly for subgroup' do - subgroup = create(:group, parent: group) - relative_path = "groups/#{subgroup.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - - doc = raw_filter(upload_link, { project: nil, group: subgroup }) - - expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}") - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com'), filter_context) - - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - end - - context 'when project or group context does not exist' do - let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } - - it 'does not raise error' do - expect { raw_filter(upload_link, project: nil) }.not_to raise_error - end - - it 'does not rewrite link' do - doc = raw_filter(upload_link, project: nil) - - expect(doc.to_html).to eq upload_link - end - end -end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index fc03741976e..2f86a046d28 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -34,11 +34,11 @@ describe Banzai::Filter::UserReferenceFilter do let(:reference) { User.reference_prefix + 'all' } before do - project.team << [project.creator, :developer] + project.add_developer(project.creator) end it 'supports a special @all mention' do - project.team << [user, :developer] + project.add_developer(user) doc = reference_filter("Hey #{reference}", author: user) expect(doc.css('a').length).to eq 1 @@ -47,7 +47,7 @@ describe Banzai::Filter::UserReferenceFilter do end it 'includes a data-author attribute when there is an author' do - project.team << [user, :developer] + project.add_developer(user) doc = reference_filter(reference, author: user) expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s) @@ -146,7 +146,7 @@ describe Banzai::Filter::UserReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)}) end it 'includes default classes' do @@ -172,7 +172,7 @@ describe Banzai::Filter::UserReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/) + expect(doc.to_html).to match(%r{\(<a.+>User</a>\.\)}) end it 'includes a data-user attribute' do diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 9596f004052..50d053011b3 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -10,15 +10,23 @@ describe Banzai::Filter::WikiLinkFilter do it "doesn't rewrite absolute links" do filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com:8000/') end + it "doesn't rewrite links to project uploads" do + filtered_link = filter("<a href='/uploads/a.test'>Link</a>", project_wiki: wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') + end + describe "invalid links" do invalid_links = ["http://:8080", "http://", "http://:8080/path"] invalid_links.each do |invalid_link| it "doesn't rewrite invalid invalid_links like #{invalid_link}" do filtered_link = filter("<a href='#{invalid_link}'>Link</a>", project_wiki: wiki).children[0] + expect(filtered_link.attribute('href').value).to eq(invalid_link) end end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index e49726aca6c..b079a3be029 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -63,8 +63,8 @@ describe Banzai::ReferenceParser::UserParser do let(:contributor) { create(:user) } before do - project.team << [user, :developer] - project.team << [contributor, :developer] + project.add_developer(user) + project.add_developer(contributor) end it 'returns the members of a project' do @@ -162,7 +162,7 @@ describe Banzai::ReferenceParser::UserParser do context 'when the link has a data-author attribute' do it 'returns the nodes when the user is a member of the project' do other_project = create(:project) - other_project.team << [user, :developer] + other_project.add_developer(user) link['data-project'] = other_project.id.to_s link['data-author'] = user.id.to_s diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb index c44bc1840df..ebd907ecb7f 100644 --- a/spec/lib/file_size_validator_spec.rb +++ b/spec/lib/file_size_validator_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe FileSizeValidator do let(:validator) { described_class.new(options) } - let(:attachment) { AttachmentUploader.new } let(:note) { create(:note) } + let(:attachment) { AttachmentUploader.new(note) } describe 'options uses an integer' do let(:options) { { maximum: 10, attributes: { attachment: attachment } } } diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb new file mode 100644 index 00000000000..ed5d56e91d4 --- /dev/null +++ b/spec/lib/gitaly/server_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitaly::Server do + describe '.all' do + let(:storages) { Gitlab.config.repositories.storages } + + it 'includes all storages' do + expect(storages.count).to eq(described_class.all.count) + expect(storages.keys).to eq(described_class.all.map(&:storage)) + end + end + + subject { described_class.all.first } + + it { is_expected.to respond_to(:server_version) } + it { is_expected.to respond_to(:git_binary_version) } + it { is_expected.to respond_to(:up_to_date?) } + it { is_expected.to respond_to(:address) } + + describe 'request memoization' do + context 'when requesting multiple properties', :request_store do + it 'uses memoization for the info request' do + expect do + subject.server_version + subject.up_to_date? + end.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + end +end diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb new file mode 100644 index 00000000000..726a3c1c83a --- /dev/null +++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::Auth::BlockedUserTracker do + set(:user) { create(:user) } + + describe '.log_if_user_blocked' do + it 'does not log if user failed to login due to undefined reason' do + expect_any_instance_of(SystemHooksService).not_to receive(:execute_hooks_for) + + expect(described_class.log_if_user_blocked({})).to be_nil + end + + it 'gracefully handles malformed environment variables' do + env = { 'warden.options' => 'test' } + + expect(described_class.log_if_user_blocked(env)).to be_nil + end + + context 'failed login due to blocked user' do + let(:env) do + { + 'warden.options' => { message: User::BLOCKED_MESSAGE }, + described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } + } + end + + subject { described_class.log_if_user_blocked(env) } + + before do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login) + end + + it 'logs a blocked user' do + user.block! + + expect(subject).to be_truthy + end + + it 'logs a blocked user by e-mail' do + user.block! + env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email + + expect(subject).to be_truthy + end + + it 'logs a LDAP blocked user' do + user.ldap_block! + + expect(subject).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 4637816570c..2733eef6611 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -76,6 +76,16 @@ describe Gitlab::Auth::UserAuthFinders do expect(find_user_from_rss_token).to be_nil end end + + context 'when the request format is empty' do + it 'the method call does not modify the original value' do + env['action_dispatch.request.formats'] = nil + + find_user_from_rss_token + + expect(env['action_dispatch.request.formats']).to be_nil + end + end end describe '#find_user_from_access_token' do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index a6fbec295b5..cc202ce8bca 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -136,8 +136,8 @@ describe Gitlab::Auth do it 'grants deploy key write permissions' do project = create(:project) - key = create(:deploy_key, can_push: true) - create(:deploy_keys_project, deploy_key: key, project: project) + key = create(:deploy_key) + create(:deploy_keys_project, :write_access, deploy_key: key, project: project) token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") @@ -146,7 +146,7 @@ describe Gitlab::Auth do it 'does not grant deploy key write permissions' do project = create(:project) - key = create(:deploy_key, can_push: true) + key = create(:deploy_key) token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") diff --git a/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb b/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb new file mode 100644 index 00000000000..21a791f5695 --- /dev/null +++ b/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount, :migration, schema: 20180105212544 do + let(:projects_table) { table(:projects) } + let(:merge_requests_table) { table(:merge_requests) } + let(:merge_request_diffs_table) { table(:merge_request_diffs) } + let(:merge_request_diff_commits_table) { table(:merge_request_diff_commits) } + + let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') } + let(:merge_request) do + merge_requests_table.create!(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: 'mr name') + end + + def create_diff!(name, commits: 0) + mr_diff = merge_request_diffs_table.create!( + merge_request_id: merge_request.id) + + commits.times do |i| + merge_request_diff_commits_table.create!( + merge_request_diff_id: mr_diff.id, + relative_order: i, sha: i) + end + + mr_diff + end + + describe '#perform' do + it 'migrates diffs that have no commits' do + diff = create_diff!('with_multiple_commits', commits: 0) + + subject.perform(diff.id, diff.id) + + expect(diff.reload.commits_count).to eq(0) + end + + it 'migrates multiple diffs to the correct values' do + diffs = Array.new(3).map.with_index { |_, i| create_diff!(i, commits: 3) } + + subject.perform(diffs.first.id, diffs.last.id) + + diffs.each do |diff| + expect(diff.reload.commits_count).to eq(3) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb index 5c471cbdeda..9bae7e53b71 100644 --- a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb @@ -24,17 +24,12 @@ describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :mig redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') end - it 'deletes the conflicting redirect_routes in the range' do + # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252 + it 'NO-OP: does not delete any redirect_routes' do expect(redirect_routes.count).to eq(8) - expect do - described_class.new.perform(1, 3) - end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2) + described_class.new.perform(1, 5) - expect do - described_class.new.perform(4, 5) - end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0) - - expect(redirect_routes.count).to eq(3) + expect(redirect_routes.count).to eq(8) end end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 84d9e635810..c8df6dd2118 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do +describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } @@ -10,6 +10,15 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t let(:merge_request_diff) { MergeRequest.find(merge_request.id).create_merge_request_diff } let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + + after do + [Project, MergeRequest, MergeRequestDiff].each(&:reset_column_information) + end + def diffs_to_hashes(diffs) diffs.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS).map(&:with_indifferent_access) end diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 7351d45336a..5432d270555 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -281,6 +281,17 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati migration.process_event(event) end + + it 'handles an error gracefully' do + event1 = create_push_event(project, author, { commits: [] }) + + expect(migration).to receive(:replicate_event).and_call_original + expect(migration).to receive(:create_push_event_payload).and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key') + + migration.process_event(event1) + + expect(described_class::EventForMigration.all.count).to eq(0) + end end describe '#replicate_event' do @@ -335,9 +346,8 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati it 'does not create push event payloads for removed events' do allow(event).to receive(:id).and_return(-1) - payload = migration.create_push_event_payload(event) + expect { migration.create_push_event_payload(event) }.to raise_error(ActiveRecord::InvalidForeignKey) - expect(payload).to be_nil expect(PushEventPayload.count).to eq(0) end diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 7b5a00c6111..021e1d14b18 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do +describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do let(:migration) { described_class.new } before do @@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do end describe '#perform' do - it 'renames the path of system-uploads', :truncate do + it 'renames the path of system-uploads' do upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg') migration.perform('uploads/system/', 'uploads/-/system/') diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb new file mode 100644 index 00000000000..4cdb679c97f --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -0,0 +1,134 @@ +require 'rails_helper' + +describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do + # commits_count attribute is added in a next migration + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + + after do + [MergeRequest, MergeRequestDiff].each(&:reset_column_information) + end + + describe '#perform' do + let(:mr_with_event) { create(:merge_request) } + let!(:merged_event) { create(:event, :merged, target: mr_with_event) } + let!(:closed_event) { create(:event, :closed, target: mr_with_event) } + + before do + # Make sure no metrics are created and kept through after_* callbacks. + mr_with_event.metrics.destroy! + end + + it 'inserts metrics and updates closed and merged events' do + subject.perform(mr_with_event.id, mr_with_event.id) + + mr_with_event.reload + + expect(mr_with_event.metrics).to have_attributes(latest_closed_by_id: closed_event.author_id, + merged_by_id: merged_event.author_id) + expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(closed_event.updated_at.to_s) + end + end + + describe '#insert_metrics_for_range' do + let!(:mrs_without_metrics) { create_list(:merge_request, 3) } + let!(:mrs_with_metrics) { create_list(:merge_request, 2) } + + before do + # Make sure no metrics are created and kept through after_* callbacks. + mrs_without_metrics.each { |m| m.metrics.destroy! } + end + + it 'inserts merge_request_metrics for merge_requests without one' do + expect { subject.insert_metrics_for_range(MergeRequest.first.id, MergeRequest.last.id) } + .to change(MergeRequest::Metrics, :count).from(2).to(5) + + mrs_without_metrics.each do |mr_without_metrics| + expect(mr_without_metrics.reload.metrics).to be_present + end + end + + it 'does not inserts merge_request_metrics for MRs out of given range' do + expect { subject.insert_metrics_for_range(mrs_with_metrics.first.id, mrs_with_metrics.last.id) } + .not_to change(MergeRequest::Metrics, :count).from(2) + end + end + + describe '#update_metrics_with_events_data' do + context 'closed events data update' do + let(:users) { create_list(:user, 3) } + let(:mrs_with_event) { create_list(:merge_request, 3) } + + before do + create_list(:event, 2, :closed, author: users.first, target: mrs_with_event.first) + create_list(:event, 3, :closed, author: users.second, target: mrs_with_event.second) + create(:event, :closed, author: users.third, target: mrs_with_event.third) + end + + it 'migrates multiple MR metrics with closed event data' do + mr_without_event = create(:merge_request) + create(:event, :merged) + + subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id) + + mrs_with_event.each do |mr_with_event| + latest_event = Event.where(action: 3, target: mr_with_event).last + + mr_with_event.metrics.reload + + expect(mr_with_event.metrics.latest_closed_by).to eq(latest_event.author) + expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(latest_event.updated_at.to_s) + end + + expect(mr_without_event.metrics.reload).to have_attributes(latest_closed_by_id: nil, + latest_closed_at: nil) + end + + it 'does not updates metrics out of given range' do + out_of_range_mr = create(:merge_request) + create(:event, :closed, author: users.last, target: out_of_range_mr) + + expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) } + .not_to change { out_of_range_mr.metrics.reload.merged_by } + .from(nil) + end + end + + context 'merged events data update' do + let(:users) { create_list(:user, 3) } + let(:mrs_with_event) { create_list(:merge_request, 3) } + + before do + create_list(:event, 2, :merged, author: users.first, target: mrs_with_event.first) + create_list(:event, 3, :merged, author: users.second, target: mrs_with_event.second) + create(:event, :merged, author: users.third, target: mrs_with_event.third) + end + + it 'migrates multiple MR metrics with merged event data' do + mr_without_event = create(:merge_request) + create(:event, :merged) + + subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id) + + mrs_with_event.each do |mr_with_event| + latest_event = Event.where(action: Event::MERGED, target: mr_with_event).last + + expect(mr_with_event.metrics.reload.merged_by).to eq(latest_event.author) + end + + expect(mr_without_event.metrics.reload).to have_attributes(merged_by_id: nil) + end + + it 'does not updates metrics out of given range' do + out_of_range_mr = create(:merge_request) + create(:event, :merged, author: users.last, target: out_of_range_mr) + + expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) } + .not_to change { out_of_range_mr.metrics.reload.merged_by } + .from(nil) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index cd3f1a45270..370c2490b97 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -2,21 +2,10 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do include TrackUntrackedUploadsHelpers + include MigrationsHelpers let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - matcher :be_scheduled_migration do |*expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - before do DatabaseCleaner.clean @@ -34,6 +23,27 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do end end + # E.g. The installation is in use at the time of migration, and someone has + # just uploaded a file + shared_examples 'does not add files in /uploads/tmp' do + let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } + + before do + FileUtils.mkdir(File.dirname(tmp_file)) + FileUtils.touch(tmp_file) + end + + after do + FileUtils.rm(tmp_file) + end + + it 'does not add files from /uploads/tmp' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + end + it 'ensures the untracked_files_for_uploads table exists' do expect do described_class.new.perform @@ -120,24 +130,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do end end - # E.g. The installation is in use at the time of migration, and someone has - # just uploaded a file context 'when there are files in /uploads/tmp' do - let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } - - before do - FileUtils.touch(tmp_file) - end - - after do - FileUtils.rm(tmp_file) - end - - it 'does not add files from /uploads/tmp' do - described_class.new.perform - - expect(untracked_files_for_uploads.count).to eq(5) - end + it_behaves_like 'does not add files in /uploads/tmp' end end end @@ -208,24 +202,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do end end - # E.g. The installation is in use at the time of migration, and someone has - # just uploaded a file context 'when there are files in /uploads/tmp' do - let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } - - before do - FileUtils.touch(tmp_file) - end - - after do - FileUtils.rm(tmp_file) - end - - it 'does not add files from /uploads/tmp' do - described_class.new.perform - - expect(untracked_files_for_uploads.count).to eq(5) - end + it_behaves_like 'does not add files in /uploads/tmp' end end end diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb index 383bae6e087..d9c21a22590 100644 --- a/spec/lib/gitlab/badge/coverage/template_spec.rb +++ b/spec/lib/gitlab/badge/coverage/template_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Badge::Coverage::Template do - let(:badge) { double(entity: 'coverage', status: 90) } + let(:badge) { double(entity: 'coverage', status: 90.00) } let(:template) { described_class.new(badge) } describe '#key_text' do @@ -13,7 +13,17 @@ describe Gitlab::Badge::Coverage::Template do describe '#value_text' do context 'when coverage is known' do it 'returns coverage percentage' do - expect(template.value_text).to eq '90%' + expect(template.value_text).to eq '90.00%' + end + end + + context 'when coverage is known to many digits' do + before do + allow(badge).to receive(:status).and_return(92.349) + end + + it 'returns rounded coverage percentage' do + expect(template.value_text).to eq '92.35%' end end @@ -37,7 +47,7 @@ describe Gitlab::Badge::Coverage::Template do describe '#value_width' do context 'when coverage is known' do it 'is narrower when coverage is known' do - expect(template.value_width).to eq 36 + expect(template.value_width).to eq 54 end end @@ -113,7 +123,7 @@ describe Gitlab::Badge::Coverage::Template do describe '#width' do context 'when coverage is known' do it 'returns the key width plus value width' do - expect(template.width).to eq 98 + expect(template.width).to eq 116 end end diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 8a83e446935..f302e412a6e 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -68,14 +68,24 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do expect(Project.find_by_full_path(project_path)).not_to be_nil end - it 'creates the Git repo in disk' do - FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git")) + it 'does not schedule an import' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + importer.create_project_if_needed + end + + it 'creates the Git repo on disk with the proper symlink for hooks' do + create_bare_repository("#{project_path}.git") importer.create_project_if_needed project = Project.find_by_full_path(project_path) + repo_path = File.join(project.repository_storage_path, project.disk_path + '.git') + hook_path = File.join(repo_path, 'hooks') - expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git')) + expect(File).to exist(repo_path) + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end context 'hashed storage enabled' do @@ -124,13 +134,14 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do end it 'creates the Git repo in disk' do - FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git")) + create_bare_repository("#{project_path}.git") importer.create_project_if_needed project = Project.find_by_full_path("#{admin.full_path}/#{project_path}") expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git')) + expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git')) end it 'moves an existing project to the correct path' do @@ -158,8 +169,11 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do it_behaves_like 'importing a repository' it 'creates the Wiki git repo in disk' do - FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git")) - FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.wiki.git")) + create_bare_repository("#{project_path}.git") + create_bare_repository("#{project_path}.wiki.git") + + expect(Projects::CreateService).to receive(:new).with(admin, hash_including(skip_wiki: true, + import_type: 'bare_repository')).and_call_original importer.create_project_if_needed @@ -182,4 +196,9 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do end end end + + def create_bare_repository(project_path) + repo_path = File.join(base_dir, project_path) + Gitlab::Git::Repository.create(repo_path, bare: true) + end end diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index 61b73abcba4..9f42cf1dfca 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -1,58 +1,122 @@ require 'spec_helper' describe ::Gitlab::BareRepositoryImport::Repository do - let(:project_repo_path) { described_class.new('/full/path/', '/full/path/to/repo.git') } + context 'legacy storage' do + subject { described_class.new('/full/path/', '/full/path/to/repo.git') } - it 'stores the repo path' do - expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git') - end + it 'stores the repo path' do + expect(subject.repo_path).to eq('/full/path/to/repo.git') + end - it 'stores the group path' do - expect(project_repo_path.group_path).to eq('to') - end + it 'stores the group path' do + expect(subject.group_path).to eq('to') + end - it 'stores the project name' do - expect(project_repo_path.project_name).to eq('repo') - end + it 'stores the project name' do + expect(subject.project_name).to eq('repo') + end - it 'stores the wiki path' do - expect(project_repo_path.wiki_path).to eq('/full/path/to/repo.wiki.git') - end + it 'stores the wiki path' do + expect(subject.wiki_path).to eq('/full/path/to/repo.wiki.git') + end + + describe '#processable?' do + it 'returns false if it is a wiki' do + subject = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git') + + expect(subject).not_to be_processable + end + + it 'returns true if group path is missing' do + subject = described_class.new('/full/path/', '/full/path/repo.git') - describe '#wiki?' do - it 'returns true if it is a wiki' do - wiki_path = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git') + expect(subject).to be_processable + end - expect(wiki_path.wiki?).to eq(true) + it 'returns true when group path and project name are present' do + expect(subject).to be_processable + end end - it 'returns false if it is not a wiki' do - expect(project_repo_path.wiki?).to eq(false) + describe '#project_full_path' do + it 'returns the project full path with trailing slash in the root path' do + expect(subject.project_full_path).to eq('to/repo') + end + + it 'returns the project full path with no trailing slash in the root path' do + subject = described_class.new('/full/path', '/full/path/to/repo.git') + + expect(subject.project_full_path).to eq('to/repo') + end end end - describe '#hashed?' do - it 'returns true if it is a hashed folder' do - path = described_class.new('/full/path/', '/full/path/@hashed/my.repo.git') + context 'hashed storage' do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:repository_storage) { 'default' } + let(:root_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } + let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" } + let(:repo_path) { File.join(root_path, "#{hashed_path}.git") } + let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") } - expect(path.hashed?).to eq(true) + before do + gitlab_shell.add_repository(repository_storage, hashed_path) + repository = Rugged::Repository.new(repo_path) + repository.config['gitlab.fullpath'] = 'to/repo' end - it 'returns false if it is not a hashed folder' do - expect(project_repo_path.hashed?).to eq(false) + after do + gitlab_shell.remove_repository(root_path, hashed_path) end - end - describe '#project_full_path' do - it 'returns the project full path' do - expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git') - expect(project_repo_path.project_full_path).to eq('to/repo') + subject { described_class.new(root_path, repo_path) } + + it 'stores the repo path' do + expect(subject.repo_path).to eq(repo_path) + end + + it 'stores the wiki path' do + expect(subject.wiki_path).to eq(wiki_path) + end + + it 'reads the group path from .git/config' do + expect(subject.group_path).to eq('to') + end + + it 'reads the project name from .git/config' do + expect(subject.project_name).to eq('repo') end - it 'with no trailing slash in the root path' do - repo_path = described_class.new('/full/path', '/full/path/to/repo.git') + describe '#processable?' do + it 'returns false if it is a wiki' do + subject = described_class.new(root_path, wiki_path) + + expect(subject).not_to be_processable + end + + it 'returns false when group and project name are missing' do + repository = Rugged::Repository.new(repo_path) + repository.config.delete('gitlab.fullpath') + + expect(subject).not_to be_processable + end + + it 'returns true when group path and project name are present' do + expect(subject).to be_processable + end + end + + describe '#project_full_path' do + it 'returns the project full path with trailing slash in the root path' do + expect(subject.project_full_path).to eq('to/repo') + end + + it 'returns the project full path with no trailing slash in the root path' do + subject = described_class.new(root_path[0...-1], repo_path) - expect(repo_path.project_full_path).to eq('to/repo') + expect(subject.project_full_path).to eq('to/repo') + end end end end diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb index fa1575e2177..f90c2d6aded 100644 --- a/spec/lib/gitlab/checks/project_moved_spec.rb +++ b/spec/lib/gitlab/checks/project_moved_spec.rb @@ -35,6 +35,12 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do project_moved = described_class.new(project, user, 'foo/bar', 'http') expect(project_moved.add_redirect_message).to eq("OK") end + + it 'should handle anonymous clones' do + project_moved = described_class.new(project, nil, 'foo/bar', 'http') + + expect(project_moved.add_redirect_message).to eq(nil) + end end describe '#redirect_message' do diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 33540eab5d6..7549e9941b6 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -120,6 +120,10 @@ describe Gitlab::Ci::Ansi2html do expect(convert_html("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>') end + it "can print 256 xterm fg bold colors" do + expect(convert_html("\e[38;5;16;1mHello")).to eq('<span class="xterm-fg-16 term-bold">Hello</span>') + end + it "can print 256 xterm bg colors on normal magenta foreground" do expect(convert_html("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') end @@ -213,11 +217,58 @@ describe Gitlab::Ci::Ansi2html do "#{section_end[0...-5]}</div>" end - it "prints light red" do - text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" - html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + shared_examples 'forbidden char in section_name' do + it 'ignores sections' do + text = "#{section_start}Some text#{section_end}" + html = text.gsub("\033[0K", '').gsub('<', '<') + + expect(convert_html(text)).to eq(html) + end + end + + shared_examples 'a legit section' do + let(:text) { "#{section_start}Some text#{section_end}" } + + it 'prints light red' do + text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" + html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + + expect(convert_html(text)).to eq(html) + end + + it 'begins with a section_start html marker' do + expect(convert_html(text)).to start_with(section_start_html) + end + + it 'ends with a section_end html marker' do + expect(convert_html(text)).to end_with(section_end_html) + end + end + + it_behaves_like 'a legit section' + + context 'section name includes $' do + let(:section_name) { 'my_$ection'} + + it_behaves_like 'forbidden char in section_name' + end + + context 'section name includes <' do + let(:section_name) { '<a_tag>'} + + it_behaves_like 'forbidden char in section_name' + end + + context 'section name contains .-_' do + let(:section_name) { 'a.Legit-SeCtIoN_namE' } + + it_behaves_like 'a legit section' + end + + it 'do not allow XSS injections' do + text = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}" - expect(convert_html(text)).to eq(html) + expect(convert_html(text)).not_to include('<script>') end end diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb index 5d4de60bc8a..3cbf19bea8b 100644 --- a/spec/lib/gitlab/ci/config/entry/key_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb @@ -4,6 +4,26 @@ describe Gitlab::Ci::Config::Entry::Key do let(:entry) { described_class.new(config) } describe 'validations' do + shared_examples 'key with slash' do + it 'is invalid' do + expect(entry).not_to be_valid + end + + it 'reports errors with config value' do + expect(entry.errors).to include 'key config cannot contain the "/" character' + end + end + + shared_examples 'key with only dots' do + it 'is invalid' do + expect(entry).not_to be_valid + end + + it 'reports errors with config value' do + expect(entry.errors).to include 'key config cannot be "." or ".."' + end + end + context 'when entry config value is correct' do let(:config) { 'test' } @@ -30,6 +50,48 @@ describe Gitlab::Ci::Config::Entry::Key do end end end + + context 'when entry value contains slash' do + let(:config) { 'key/with/some/slashes' } + + it_behaves_like 'key with slash' + end + + context 'when entry value contains URI encoded slash (%2F)' do + let(:config) { 'key%2Fwith%2Fsome%2Fslashes' } + + it_behaves_like 'key with slash' + end + + context 'when entry value is a dot' do + let(:config) { '.' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is two dots' do + let(:config) { '..' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is a URI encoded dot (%2E)' do + let(:config) { '%2e' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is two URI encoded dots (%2E)' do + let(:config) { '%2E%2e' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is one dot and one URI encoded dot' do + let(:config) { '.%2e' } + + it_behaves_like 'key with only dots' + end end describe '.default' do diff --git a/spec/lib/gitlab/ci/status/build/action_spec.rb b/spec/lib/gitlab/ci/status/build/action_spec.rb index 8c25f72804b..d612d29e3e0 100644 --- a/spec/lib/gitlab/ci/status/build/action_spec.rb +++ b/spec/lib/gitlab/ci/status/build/action_spec.rb @@ -37,16 +37,16 @@ describe Gitlab::Ci::Status::Build::Action do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'when build is an action' do - let(:build) { create(:ci_build, :manual) } + context 'when build is playable action' do + let(:build) { create(:ci_build, :playable) } it 'is a correct match' do expect(subject).to be true end end - context 'when build is not manual' do - let(:build) { create(:ci_build) } + context 'when build is not playable action' do + let(:build) { create(:ci_build, :non_playable) } it 'does not match' do expect(subject).to be false diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb index 03d1f46b517..2cce7a23ea7 100644 --- a/spec/lib/gitlab/ci/status/build/common_spec.rb +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::Common do describe '#has_details?' do context 'when user has access to read build' do before do - project.team << [user, :developer] + project.add_developer(user) end it { is_expected.to have_details } diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb index b38fbee2486..40871f86568 100644 --- a/spec/lib/gitlab/ci/status/external/common_spec.rb +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Ci::Status::External::Common do describe '#has_details?' do context 'when user has access to read commit status' do before do - project.team << [user, :developer] + project.add_developer(user) end it { is_expected.to have_details } diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb index c96fd53e730..529d02a3e39 100644 --- a/spec/lib/gitlab/ci/status/external/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Ci::Status::External::Factory do let(:external_url) { 'http://gitlab.com/status' } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when external status has a simple core status' do diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 4a5b45e7cae..57df8325635 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Pipeline::Common do describe '#has_details?' do context 'when user has access to read pipeline' do before do - project.team << [user, :developer] + project.add_developer(user) end it { is_expected.to have_details } diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index dd754b849b2..defb3fdc0df 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do let(:factory) { described_class.new(pipeline, user) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when pipeline has a core status' do diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f5f03ac0395..6ec35f8da7e 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Ci::Status::Stage::Common do context 'when user has permission to read pipeline' do before do - project.team << [user, :master] + project.add_master(user) end it 'has details' do diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 432b07e4902..dee4f4efd1b 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Stage::Factory do end before do - project.team << [user, :developer] + project.add_developer(user) end context 'when stage has a core status' do diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index ef7d766a13d..8c79ef54c6c 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -13,8 +13,8 @@ describe Gitlab::ClosingIssueExtractor do subject { described_class.new(project, project.creator) } before do - project.team << [project.creator, :developer] - project2.team << [project.creator, :master] + project.add_developer(project.creator) + project2.add_master(project.creator) end describe "#closed_by_message" do @@ -297,7 +297,7 @@ describe Gitlab::ClosingIssueExtractor do context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') - jira_project.team << [jira_project.creator, :master] + jira_project.add_master(jira_project.creator) jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) closing_issue_extractor = described_class.new(jira_project, jira_project.creator) message = "Resolve #{jira_issue.to_reference}" diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 492659a82b0..4ddcbd7eb66 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -8,22 +8,37 @@ describe Gitlab::CurrentSettings do end describe '#current_application_settings' do + it 'allows keys to be called directly' do + db_settings = create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + + expect(described_class.home_page_url).to eq(db_settings.home_page_url) + expect(described_class.signup_enabled?).to be_falsey + expect(described_class.signup_enabled).to be_falsey + expect(described_class.metrics_sample_interval).to be(15) + end + context 'with DB available' do before do - allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true) + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) + allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original + allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true) end it 'attempts to use cached values first' do expect(ApplicationSetting).to receive(:cached) - expect(current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis returns an empty value' do expect(ApplicationSetting).to receive(:cached).and_return(nil) expect(ApplicationSetting).to receive(:last).and_call_original.twice - expect(current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis fails' do @@ -32,14 +47,14 @@ describe Gitlab::CurrentSettings do expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) - expect(current_application_settings).to eq(db_settings) + expect(described_class.current_application_settings).to eq(db_settings) end it 'creates default ApplicationSettings if none are present' do expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) - settings = current_application_settings + settings = described_class.current_application_settings expect(settings).to be_a(ApplicationSetting) expect(settings).to be_persisted @@ -52,7 +67,7 @@ describe Gitlab::CurrentSettings do end it 'returns an in-memory ApplicationSetting object' do - settings = current_application_settings + settings = described_class.current_application_settings expect(settings).to be_a(OpenStruct) expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) @@ -63,7 +78,7 @@ describe Gitlab::CurrentSettings do db_settings = create(:application_setting, home_page_url: 'http://mydomain.com', signup_enabled: false) - settings = current_application_settings + settings = described_class.current_application_settings app_defaults = ApplicationSetting.last expect(settings).to be_a(OpenStruct) @@ -80,15 +95,16 @@ describe Gitlab::CurrentSettings do context 'with DB unavailable' do before do - allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false) - allow_any_instance_of(described_class).to receive(:retrieve_settings_from_database_cache?).and_return(nil) + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) end it 'returns an in-memory ApplicationSetting object' do expect(ApplicationSetting).not_to receive(:current) expect(ApplicationSetting).not_to receive(:last) - expect(current_application_settings).to be_a(OpenStruct) + expect(described_class.current_application_settings).to be_a(OpenStruct) end end @@ -101,8 +117,8 @@ describe Gitlab::CurrentSettings do expect(ApplicationSetting).not_to receive(:current) expect(ApplicationSetting).not_to receive(:last) - expect(current_application_settings).to be_a(ApplicationSetting) - expect(current_application_settings).not_to be_persisted + expect(described_class.current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).not_to be_persisted end end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 28ea7d4c303..38a47a159e1 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -122,17 +122,18 @@ describe 'cycle analytics events' do let(:stage) { :test } let(:merge_request) { MergeRequest.first } + let!(:pipeline) do create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, - project: context.project, + project: project, head_pipeline_of: merge_request) end before do - create(:ci_build, pipeline: pipeline, status: :success, author: user) - create(:ci_build, pipeline: pipeline, status: :success, author: user) + create(:ci_build, :success, pipeline: pipeline, author: user) + create(:ci_build, :success, pipeline: pipeline, author: user) pipeline.run! pipeline.succeed! @@ -219,17 +220,18 @@ describe 'cycle analytics events' do describe '#staging_events' do let(:stage) { :staging } let(:merge_request) { MergeRequest.first } + let!(:pipeline) do create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, - project: context.project, + project: project, head_pipeline_of: merge_request) end before do - create(:ci_build, pipeline: pipeline, status: :success, author: user) - create(:ci_build, pipeline: pipeline, status: :success, author: user) + create(:ci_build, :success, pipeline: pipeline, author: user) + create(:ci_build, :success, pipeline: pipeline, author: user) pipeline.run! pipeline.succeed! diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb index 2a0dd7be439..6de4bd3dc7c 100644 --- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::CycleAnalytics::Permissions do context 'user is master' do before do - project.team << [user, :master] + project.add_master(user) end it 'has permissions to issue stage' do @@ -72,7 +72,7 @@ describe Gitlab::CycleAnalytics::Permissions do context 'user has no build permissions' do before do - project.team << [user, :guest] + project.add_guest(user) end it 'has permissions to issue stage' do @@ -90,7 +90,7 @@ describe Gitlab::CycleAnalytics::Permissions do context 'user has no merge request permissions' do before do - project.team << [user, :guest] + project.add_guest(user) end it 'has permissions to issue stage' do @@ -108,7 +108,7 @@ describe Gitlab::CycleAnalytics::Permissions do context 'user has no issue permissions' do before do - project.team << [user, :developer] + project.add_developer(user) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 664ba0f7234..1de3a14b809 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -902,7 +902,7 @@ describe Gitlab::Database::MigrationHelpers do describe '#check_trigger_permissions!' do it 'does nothing when the user has the correct permissions' do expect { model.check_trigger_permissions!('users') } - .not_to raise_error(RuntimeError) + .not_to raise_error end it 'raises RuntimeError when the user does not have the correct permissions' do @@ -1006,12 +1006,12 @@ describe Gitlab::Database::MigrationHelpers do context 'with batch_size option' do it 'queues jobs correctly' do Sidekiq::Testing.fake! do - model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2) + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2) expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f) end end end @@ -1019,10 +1019,10 @@ describe Gitlab::Database::MigrationHelpers do context 'without batch_size option' do it 'queues jobs correctly' do Sidekiq::Testing.fake! do - model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds) + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes) expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f) end end end @@ -1036,4 +1036,93 @@ describe Gitlab::Database::MigrationHelpers do end end end + + describe '#change_column_type_using_background_migration' do + let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) } + + let(:issue_model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'issues' + include EachBatch + end + end + + it 'changes the type of a column using a background migration' do + expect(model) + .to receive(:add_column) + .with('issues', 'closed_at_for_type_change', :datetime_with_timezone) + + expect(model) + .to receive(:install_rename_triggers) + .with('issues', :closed_at, 'closed_at_for_type_change') + + expect(BackgroundMigrationWorker) + .to receive(:perform_in) + .ordered + .with( + 10.minutes, + 'CopyColumn', + ['issues', :closed_at, 'closed_at_for_type_change', issue.id, issue.id] + ) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in) + .ordered + .with( + 1.hour + 10.minutes, + 'CleanupConcurrentTypeChange', + ['issues', :closed_at, 'closed_at_for_type_change'] + ) + + expect(Gitlab::BackgroundMigration) + .to receive(:steal) + .ordered + .with('CopyColumn') + + expect(Gitlab::BackgroundMigration) + .to receive(:steal) + .ordered + .with('CleanupConcurrentTypeChange') + + model.change_column_type_using_background_migration( + issue_model.all, + :closed_at, + :datetime_with_timezone + ) + end + end + + describe '#perform_background_migration_inline?' do + it 'returns true in a test environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(true) + + expect(model.perform_background_migration_inline?).to eq(true) + end + + it 'returns true in a development environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(false) + + allow(Rails.env) + .to receive(:development?) + .and_return(true) + + expect(model.perform_background_migration_inline?).to eq(true) + end + + it 'returns false in a production environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(false) + + allow(Rails.env) + .to receive(:development?) + .and_return(false) + + expect(model.perform_background_migration_inline?).to eq(false) + end + end end 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 596cc435bd9..cc7cb3f23fd 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 @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index 1143182531f..f31475dbd71 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } let(:namespace) { create(:group, name: 'the-path') } diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index e850b5cd6a4..0958144643b 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } let(:project) do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index 7695b95dc57..1d31f96159c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -13,7 +13,7 @@ shared_examples 'renames child namespaces' do |type| end end -describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do let(:subject) { FakeRenameReservedPathMigrationV1.new } before do diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index d81774c8b8f..a067c42b75b 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -19,4 +19,18 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do diff_files end + + shared_examples 'initializes a DiffCollection' do + it 'returns a valid instance of a DiffCollection' do + expect(diff_files).to be_a(Gitlab::Git::DiffCollection) + end + end + + context 'with Gitaly disabled', :disable_gitaly do + it_behaves_like 'initializes a DiffCollection' + end + + context 'with Gitaly enabled' do + it_behaves_like 'initializes a DiffCollection' + end end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index ff9acfd08b9..9204ea37963 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -431,4 +431,29 @@ describe Gitlab::Diff::File do end end end + + context 'when neither blob exists' do + let(:blank_diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: Gitlab::Git::BLANK_SHA, head_sha: Gitlab::Git::BLANK_SHA) } + let(:diff_file) { described_class.new(diff, diff_refs: blank_diff_refs, repository: project.repository) } + + describe '#blob' do + it 'returns a concrete nil so it can be used in boolean expressions' do + actual = diff_file.blob && true + + expect(actual).to be_nil + end + end + + describe '#binary?' do + it 'returns false' do + expect(diff_file).not_to be_binary + end + end + + describe '#size' do + it 'returns zero' do + expect(diff_file.size).to be_zero + end + end + end end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index d0fa16ce4d1..031efcf1291 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler do context 'and current user can update noteable' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'does not raise an error' do @@ -99,7 +99,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler do context 'and current user can update noteable' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'post a note and updates the noteable' do diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index f6e5c55240f..4e9367323cb 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -120,6 +120,24 @@ describe Gitlab::EncodingHelper do it 'returns empty string on conversion errors' do expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) end + + context 'with strings that can be forcefully encoded into utf8' do + let(:test_string) do + "refs/heads/FixSymbolsTitleDropdown".encode("ASCII-8BIT") + end + let(:expected_string) do + "refs/heads/FixSymbolsTitleDropdown".encode("UTF-8") + end + + subject { ext_class.encode_utf8(test_string) } + + it "doesn't use CharlockHolmes if the encoding can be forced into utf_8" do + expect(CharlockHolmes::EncodingDetector).not_to receive(:detect) + + expect(subject).to eq(expected_string) + expect(subject.encoding.name).to eq('UTF-8') + end + end end describe '#clean' do @@ -145,4 +163,18 @@ describe Gitlab::EncodingHelper do end end end + + describe 'encode_binary' do + [ + [nil, ""], + ["", ""], + [" ", " "], + %w(a1 a1), + ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] + ].each do |input, result| + it "encodes #{input.inspect} to #{result.inspect}" do + expect(ext_class.encode_binary(input)).to eq(result) + end + end + end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index 7322a326b01..6193e177668 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -73,4 +73,19 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do described_class.new(key, timeout: 3600).try_obtain end end + + describe '#ttl' do + it 'returns the TTL of the Redis key' do + lease = described_class.new('kittens', timeout: 100) + lease.try_obtain + + expect(lease.ttl <= 100).to eq(true) + end + + it 'returns nil when the lease does not exist' do + lease = described_class.new('kittens', timeout: 10) + + expect(lease.ttl).to be_nil + end + end end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 7dc06c90078..4d2f08f95fc 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::Gfm::ReferenceRewriter do let(:text) { 'some text' } before do - old_project.team << [user, :reporter] + old_project.add_reporter(user) end describe '#rewrite' do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index 39e3b875c49..326ed2f2ecf 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::Gfm::UploadsRewriter do end let(:text) do - "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}" + "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}" end describe '#rewrite' do diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb new file mode 100644 index 00000000000..5d22dcfb508 --- /dev/null +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + + subject { described_class.new(repository, 'lfs') } + + it 'loads .gitattributes blob' do + repository.raw # Initialize repository in advance since this also checks attributes + + expected_filter = 'filter=lfs diff=lfs merge=lfs' + receive_blob = receive(:new).with(a_string_including(expected_filter)) + expect(Gitlab::Git::AttributesParser).to receive_blob.and_call_original + + subject + end + + it 'handles missing blobs' do + expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error + end + + describe '#attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.lfs')['filter']).to eq('lfs') + end + end +end diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index b715fc3410a..323334e99a5 100644 --- a/spec/lib/gitlab/git/attributes_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -describe Gitlab::Git::Attributes, seed_helper: true do - let(:path) do - File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') - end +describe Gitlab::Git::AttributesParser, seed_helper: true do + let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') } + let(:data) { File.read(attributes_path) } - subject { described_class.new(path) } + subject { described_class.new(data) } describe '#attributes' do context 'using a path with attributes' do @@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do expect(subject.attributes('test.foo')).to eq({}) end end + + context 'when attributes data is a file handle' do + subject do + File.open(attributes_path, 'r') do |file_handle| + described_class.new(file_handle) + end + end + + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + end + + context 'when attributes data is nil' do + let(:data) { nil } + + it 'returns an empty Hash' do + expect(subject.attributes('test.foo')).to eq({}) + end + end end describe '#patterns' do @@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'parses an entry that uses a tab to separate the pattern and attributes' do - expect(subject.patterns[File.join(path, '*.md')]) + expect(subject.patterns[File.join('/', '*.md')]) .to eq({ 'gitlab-language' => 'markdown' }) end it 'stores patterns in reverse order' do first = subject.patterns.to_a[0] - expect(first[0]).to eq(File.join(path, 'bla/bla.txt')) + expect(first[0]).to eq(File.join('/', 'bla/bla.txt')) end # It's a bit hard to test for something _not_ being processed. As such we'll @@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'ignores any comments and empty lines' do expect(subject.patterns.length).to eq(10) end - - it 'does not parse anything when the attributes file does not exist' do - expect(File).to receive(:exist?) - .with(File.join(path, 'info/attributes')) - .and_return(false) - - expect(subject.patterns).to eq({}) - end end describe '#parse_attributes' do @@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) end - it 'does not yield when the attributes file does not exist' do - expect(File).to receive(:exist?) - .with(File.join(path, 'info/attributes')) - .and_return(false) - - expect { |b| subject.each_line(&b) }.not_to yield_control - end - it 'does not yield when the attributes file has an unsupported encoding' do - path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') - attrs = described_class.new(path) + path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes') + attrs = described_class.new(File.read(path)) expect { |b| attrs.each_line(&b) }.not_to yield_control end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index c04a9688503..8ac960133c5 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -16,6 +16,18 @@ describe Gitlab::Git::Blob, seed_helper: true do end shared_examples 'finding blobs' do + context 'nil path' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) } + + it { expect(blob).to eq(nil) } + end + + context 'blank path' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') } + + it { expect(blob).to eq(nil) } + end + context 'file in subdir' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } @@ -146,7 +158,7 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'when sha references a tree' do it 'returns nil' do - tree = Gitlab::Git::Commit.find(repository, 'master').tree + tree = repository.rugged.rev_parse('master^{tree}') blob = Gitlab::Git::Blob.raw(repository, tree.oid) @@ -202,16 +214,6 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'limiting' do subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } - context 'default' do - let(:blob_size_limit) { nil } - - it 'limits to MAX_DATA_DISPLAY_SIZE' do - stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) - - expect(subject.first.data.size).to eq(100) - end - end - context 'positive' do let(:blob_size_limit) { 10 } @@ -221,7 +223,10 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'zero' do let(:blob_size_limit) { 0 } - it { expect(subject.first.data).to eq('') } + it 'only loads the metadata' do + expect(subject.first.size).not_to be(0) + expect(subject.first.data).to eq('') + end end context 'negative' do @@ -237,7 +242,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end describe '.batch_lfs_pointers' do - let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree } + let(:tree_object) { repository.rugged.rev_parse('master^{tree}') } let(:non_lfs_blob) do Gitlab::Git::Blob.find( @@ -255,29 +260,57 @@ describe Gitlab::Git::Blob, seed_helper: true do ) end - it 'returns a list of Gitlab::Git::Blob' do - blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) + shared_examples 'fetching batch of LFS pointers' do + it 'returns a list of Gitlab::Git::Blob' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) - expect(blobs.count).to eq(1) - expect(blobs).to all( be_a(Gitlab::Git::Blob) ) - end + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end - it 'silently ignores tree objects' do - blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + it 'accepts blob IDs as a lazy enumerator' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy) - expect(blobs).to eq([]) - end + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end + + it 'handles empty list of IDs gracefully' do + blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy) + blobs_2 = described_class.batch_lfs_pointers(repository, []) + + expect(blobs_1).to eq([]) + expect(blobs_2).to eq([]) + end + + it 'silently ignores tree objects' do + blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + + expect(blobs).to eq([]) + end + + it 'silently ignores non lfs objects' do + blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + + expect(blobs).to eq([]) + end - it 'silently ignores non lfs objects' do - blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + it 'avoids loading large blobs into memory' do + # This line could call `lookup` on `repository`, so do here before mocking. + non_lfs_blob_id = non_lfs_blob.id - expect(blobs).to eq([]) + expect(repository).not_to receive(:lookup) + + described_class.batch_lfs_pointers(repository, [non_lfs_blob_id]) + end end - it 'avoids loading large blobs into memory' do - expect(repository).not_to receive(:lookup) + context 'when Gitaly batch_lfs_pointers is enabled' do + it_behaves_like 'fetching batch of LFS pointers' + end - described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do + it_behaves_like 'fetching batch of LFS pointers' end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 5ed639543e0..85e6efd7ca2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(@commit.parents).to eq(@gitlab_parents) } it { expect(@commit.parent_id).to eq(@parents.first.oid) } it { expect(@commit.no_commit_message).to eq("--no commit message") } - it { expect(@commit.tree).to eq(@tree) } after do # Erase the new commit so other tests get the original repo @@ -389,6 +388,84 @@ describe Gitlab::Git::Commit, seed_helper: true do end end end + + describe '.extract_signature' do + subject { described_class.extract_signature(repository, commit_id) } + + shared_examples '.extract_signature' do + context 'when the commit is signed' do + let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + it 'returns signature and signed text' do + signature, signed_text = subject + + expected_signature = <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG/MacGPG2 v2.0.22 (Darwin) + Comment: GPGTools - https://gpgtools.org + + iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 + Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ + mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar + TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v + hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy + ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= + =j51i + -----END PGP SIGNATURE----- + SIGNATURE + + expect(signature).to eq(expected_signature.chomp) + expect(signature).to be_a_binary_string + + expected_signed_text = <<~SIGNED_TEXT + tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae + parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f + author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + + Feature added + + Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> + SIGNED_TEXT + + expect(signed_text).to eq(expected_signed_text) + expect(signed_text).to be_a_binary_string + end + end + + context 'when the commit has no signature' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when the commit cannot be found' do + let(:commit_id) { Gitlab::Git::BLANK_SHA } + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when the commit ID is invalid' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } + + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + + context 'with gitaly' do + it_behaves_like '.extract_signature' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '.extract_signature' + end + end end describe '#init_from_rugged' do @@ -428,6 +505,11 @@ describe Gitlab::Git::Commit, seed_helper: true do subject { super().deletions } it { is_expected.to eq(6) } end + + describe '#total' do + subject { super().total } + it { is_expected.to eq(17) } + end end describe '#stats with gitaly on' do diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb index 24da9589458..f4b964e1ee9 100644 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -25,51 +25,6 @@ describe Gitlab::Git::GitlabProjects do it { expect(gl_projects.logger).to eq(logger) } end - describe '#mv_project' do - let(:new_repo_path) { File.join(tmp_repos_path, 'repo.git') } - - it 'moves a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - - message = "Moving repository from <#{tmp_repo_path}> to <#{new_repo_path}>." - expect(logger).to receive(:info).with(message) - - expect(gl_projects.mv_project('repo.git')).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - expect(File.exist?(new_repo_path)).to be_truthy - end - - it "fails if the source path doesn't exist" do - expected_source_path = File.join(tmp_repos_path, 'bad-src.git') - expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.") - - result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git') - expect(result).to be_falsy - end - - it 'fails if the destination path already exists' do - FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git')) - - expected_distination_path = File.join(tmp_repos_path, 'already-exists.git') - message = "mv-project failed: destination path <#{expected_distination_path}> already exists." - expect(logger).to receive(:error).with(message) - - expect(gl_projects.mv_project('already-exists.git')).to be_falsy - end - end - - describe '#rm_project' do - it 'removes a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - expect(logger).to receive(:info).with("Removing repository <#{tmp_repo_path}>.") - - expect(gl_projects.rm_project).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - end - end - describe '#push_branches' do let(:remote_name) { 'remote-name' } let(:branch_name) { 'master' } @@ -203,39 +158,55 @@ describe Gitlab::Git::GitlabProjects do subject { gl_projects.import_project(import_url, timeout) } - context 'success import' do - it 'imports a repo' do - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + shared_examples 'importing repository' do + context 'success import' do + it 'imports a repo' do + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." - expect(logger).to receive(:info).with(message) + is_expected.to be_truthy - is_expected.to be_truthy + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy + end + end - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy + context 'already exists' do + it "doesn't import" do + FileUtils.mkdir_p(tmp_repo_path) + + is_expected.to be_falsy + end end end - context 'already exists' do - it "doesn't import" do - FileUtils.mkdir_p(tmp_repo_path) + context 'when Gitaly import_repository feature is enabled' do + it_behaves_like 'importing repository' + end + + context 'when Gitaly import_repository feature is disabled', :disable_gitaly do + describe 'logging' do + it 'imports a repo' do + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." + expect(logger).to receive(:info).with(message) - is_expected.to be_falsy + subject + end end - end - context 'timeout' do - it 'does not import a repo' do - stub_spawn_timeout(cmd, timeout, nil) + context 'timeout' do + it 'does not import a repo' do + stub_spawn_timeout(cmd, timeout, nil) - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." - expect(logger).to receive(:error).with(message) + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." + expect(logger).to receive(:error).with(message) - is_expected.to be_falsy + is_expected.to be_falsy - expect(gl_projects.output).to eq("Timed out\n") - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + expect(gl_projects.output).to eq("Timed out\n") + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + end end + + it_behaves_like 'importing repository' end end @@ -243,49 +214,78 @@ describe Gitlab::Git::GitlabProjects do let(:dest_repos_path) { tmp_repos_path } let(:dest_repo_name) { File.join('@hashed', 'aa', 'bb', 'xyz.git') } let(:dest_repo) { File.join(dest_repos_path, dest_repo_name) } - let(:dest_namespace) { File.dirname(dest_repo) } subject { gl_projects.fork_repository(dest_repos_path, dest_repo_name) } before do FileUtils.mkdir_p(dest_repos_path) + + # Undo spec_helper stub that deletes hooks + allow_any_instance_of(described_class).to receive(:fork_repository).and_call_original end after do FileUtils.rm_rf(dest_repos_path) end - it 'forks the repository' do - message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." - expect(logger).to receive(:info).with(message) + shared_examples 'forking a repository' do + it 'forks the repository' do + is_expected.to be_truthy - is_expected.to be_truthy + expect(File.exist?(dest_repo)).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + end - expect(File.exist?(dest_repo)).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + it 'does not fork if a project of the same name already exists' do + # create a fake project at the intended destination + FileUtils.mkdir_p(dest_repo) + + is_expected.to be_falsy + end end - it 'does not fork if a project of the same name already exists' do - # create a fake project at the intended destination - FileUtils.mkdir_p(dest_repo) + context 'when Gitaly fork_repository feature is enabled' do + it_behaves_like 'forking a repository' + end - # trying to fork again should fail as the repo already exists - message = "fork-repository failed: destination repository <#{dest_repo}> already exists." - expect(logger).to receive(:error).with(message) + context 'when Gitaly fork_repository feature is disabled', :disable_gitaly do + it_behaves_like 'forking a repository' - is_expected.to be_falsy - end + # We seem to be stuck to having only one working Gitaly storage in tests, changing + # that is not very straight-forward so I'm leaving this test here for now till + # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed. + context 'different storages' do + let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), 'alternative') } - context 'different storages' do - let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), 'alternative') } + it 'forks the repo' do + is_expected.to be_truthy - it 'forks the repo' do - is_expected.to be_truthy + expect(File.exist?(dest_repo)).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + end + end - expect(File.exist?(dest_repo)).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + describe 'log messages' do + describe 'successful fork' do + it do + message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." + expect(logger).to receive(:info).with(message) + + subject + end + end + + describe 'failed fork due existing destination' do + it do + FileUtils.mkdir_p(dest_repo) + message = "fork-repository failed: destination repository <#{dest_repo}> already exists." + expect(logger).to receive(:error).with(message) + + subject + end + end end end end diff --git a/spec/lib/gitlab/git/info_attributes_spec.rb b/spec/lib/gitlab/git/info_attributes_spec.rb new file mode 100644 index 00000000000..ea84909c3e0 --- /dev/null +++ b/spec/lib/gitlab/git/info_attributes_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Git::InfoAttributes, seed_helper: true do + let(:path) do + File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') + end + + subject { described_class.new(path) } + + describe '#attributes' do + context 'using a path with attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + + it 'returns an empty Hash for a defined path without attributes' do + expect(subject.attributes('bla/bla.txt')).to eq({}) + end + end + end + + describe '#parser' do + it 'parses a file with entries' do + expect(subject.patterns).to be_an_instance_of(Hash) + expect(subject.patterns["/*.txt"]).to eq({ 'text' => true }) + end + + it 'does not parse anything when the attributes file does not exist' do + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) + + expect(subject.patterns).to eq({}) + end + + it 'does not parse attributes files with unsupported encoding' do + path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') + subject = described_class.new(path) + + expect(subject.patterns).to eq({}) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 03a9cc488ca..ec1c7a96f92 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2,6 +2,7 @@ require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do include Gitlab::EncodingHelper + using RSpec::Parameterized::TableSyntax shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| it 'wraps gRPC not found error' do @@ -18,9 +19,11 @@ describe Gitlab::Git::Repository, seed_helper: true do end let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:storage_path) { TestEnv.repos_path } + let(:user) { build(:user) } describe '.create_hooks' do - let(:repo_path) { File.join(TestEnv.repos_path, 'hook-test.git') } + let(:repo_path) { File.join(storage_path, 'hook-test.git') } let(:hooks_dir) { File.join(repo_path, 'hooks') } let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path } let(:existing_target) { File.join(repo_path, 'foobar') } @@ -247,7 +250,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end shared_examples 'archive check' do |extenstion| - it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) } + it { expect(metadata['ArchivePath']).to match(%r{tmp/gitlab-git-test.git/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}}) } it { expect(metadata['ArchivePath']).to end_with extenstion } end @@ -441,6 +444,7 @@ describe Gitlab::Git::Repository, seed_helper: true do shared_examples 'simple commit counting' do it { expect(repository.commit_count("master")).to eq(25) } it { expect(repository.commit_count("feature")).to eq(9) } + it { expect(repository.commit_count("does-not-exist")).to eq(0) } end context 'when Gitaly commit_count feature is enabled' do @@ -559,35 +563,39 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#delete_refs' do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - end + shared_examples 'deleting refs' do + let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - it 'deletes the ref' do - @repo.delete_refs('refs/heads/feature') + after do + ensure_seeds + end - expect(@repo.rugged.references['refs/heads/feature']).to be_nil - end + it 'deletes the ref' do + repo.delete_refs('refs/heads/feature') + + expect(repo.rugged.references['refs/heads/feature']).to be_nil + end - it 'deletes all refs' do - refs = %w[refs/heads/wip refs/tags/v1.1.0] - @repo.delete_refs(*refs) + it 'deletes all refs' do + refs = %w[refs/heads/wip refs/tags/v1.1.0] + repo.delete_refs(*refs) - refs.each do |ref| - expect(@repo.rugged.references[ref]).to be_nil + refs.each do |ref| + expect(repo.rugged.references[ref]).to be_nil + end end - end - it 'raises an error if it failed' do - expect(@repo).to receive(:popen).and_return(['Error', 1]) + it 'raises an error if it failed' do + expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) + end + end - expect do - @repo.delete_refs('refs/heads/fix') - end.to raise_error(Gitlab::Git::Repository::GitError) + context 'when Gitaly delete_refs feature is enabled' do + it_behaves_like 'deleting refs' end - after(:all) do - ensure_seeds + context 'when Gitaly delete_refs feature is disabled', :disable_gitaly do + it_behaves_like 'deleting refs' end end @@ -645,38 +653,47 @@ describe Gitlab::Git::Repository, seed_helper: true do end after do - Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project') + Gitlab::Shell.new.remove_repository(storage_path, 'my_project') end - it 'fetches a repository as a mirror remote' do - subject + shared_examples 'repository mirror fecthing' do + it 'fetches a repository as a mirror remote' do + subject - expect(refs(new_repository.path)).to eq(refs(repository.path)) - end + expect(refs(new_repository.path)).to eq(refs(repository.path)) + end - context 'with keep-around refs' do - let(:sha) { SeedRepo::Commit::ID } - let(:keep_around_ref) { "refs/keep-around/#{sha}" } - let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } + context 'with keep-around refs' do + let(:sha) { SeedRepo::Commit::ID } + let(:keep_around_ref) { "refs/keep-around/#{sha}" } + let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } - before do - repository.rugged.references.create(keep_around_ref, sha, force: true) - repository.rugged.references.create(tmp_ref, sha, force: true) - end + before do + repository.rugged.references.create(keep_around_ref, sha, force: true) + repository.rugged.references.create(tmp_ref, sha, force: true) + end - it 'includes the temporary and keep-around refs' do - subject + it 'includes the temporary and keep-around refs' do + subject - expect(refs(new_repository.path)).to include(keep_around_ref) - expect(refs(new_repository.path)).to include(tmp_ref) + expect(refs(new_repository.path)).to include(keep_around_ref) + expect(refs(new_repository.path)).to include(tmp_ref) + end end end + + context 'with gitaly enabled' do + it_behaves_like 'repository mirror fecthing' + end + + context 'with gitaly enabled', :skip_gitaly_mock do + it_behaves_like 'repository mirror fecthing' + end end describe '#remote_tags' do let(:remote_name) { 'upstream' } let(:target_commit_id) { SeedRepo::Commit::ID } - let(:user) { create(:user) } let(:tag_name) { 'v0.0.1' } let(:tag_message) { 'My tag' } let(:remote_repository) do @@ -701,21 +718,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#remote_exists?' do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.add_remote("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) - end - - it 'returns true for an existing remote' do - expect(@repo.remote_exists?('new_remote')).to eq(true) - end - - it 'returns false for a non-existing remote' do - expect(@repo.remote_exists?('foo')).to eq(false) - end - end - describe "#log" do let(:commit_with_old_name) do Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) @@ -903,44 +905,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context "compare results between log_by_walk and log_by_shell" do - let(:options) { { ref: "master" } } - let(:commits_by_walk) { repository.log(options).map(&:id) } - let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - - context "with limit" do - let(:options) { { ref: "master", limit: 1 } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with offset" do - let(:options) { { ref: "master", offset: 1 } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with skip_merges" do - let(:options) { { ref: "master", skip_merges: true } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with path" do - let(:options) { { ref: "master", path: "encoding" } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - - context "with follow" do - let(:options) { { ref: "master", path: "encoding", follow: true } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - end - end - context "where provides 'after' timestamp" do options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } @@ -985,6 +949,16 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end + + context 'limit validation' do + where(:limit) do + [0, nil, '', 'foo'] + end + + with_them do + it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } + end + end end describe "#rugged_commits_between" do @@ -1026,11 +1000,34 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to eq(17) } end + describe '#merge_base' do + shared_examples '#merge_base' do + where(:from, :to, :result) do + '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' + '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' + '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil + 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil + end + + with_them do + it { expect(repository.merge_base(from, to)).to eq(result) } + end + end + + context 'with gitaly' do + it_behaves_like '#merge_base' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#merge_base' + end + end + describe '#count_commits' do shared_examples 'extended commit counting' do context 'with after timestamp' do it 'returns the number of commits after timestamp' do - options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') } expect(repository.count_commits(options)).to eq(25) end @@ -1038,18 +1035,64 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with before timestamp' do it 'returns the number of commits before timestamp' do - options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } + options = { ref: 'feature', before: Time.iso8601('2015-03-03T20:15:01+00:00') } expect(repository.count_commits(options)).to eq(9) end end + context 'with max_count' do + it 'returns the number of commits with path ' do + options = { ref: 'master', max_count: 5 } + + expect(repository.count_commits(options)).to eq(5) + end + end + context 'with path' do it 'returns the number of commits with path ' do - options = { ref: 'master', limit: nil, path: "encoding" } + options = { ref: 'master', path: 'encoding' } + + expect(repository.count_commits(options)).to eq(2) + end + end + + context 'with option :from and option :to' do + it 'returns the number of commits ahead for fix-mode..fix-blob-path' do + options = { from: 'fix-mode', to: 'fix-blob-path' } expect(repository.count_commits(options)).to eq(2) end + + it 'returns the number of commits ahead for fix-blob-path..fix-mode' do + options = { from: 'fix-blob-path', to: 'fix-mode' } + + expect(repository.count_commits(options)).to eq(1) + end + + context 'with option :left_right' do + it 'returns the number of commits for fix-mode...fix-blob-path' do + options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true } + + expect(repository.count_commits(options)).to eq([1, 2]) + end + + context 'with max_count' do + it 'returns the number of commits with path ' do + options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 } + + expect(repository.count_commits(options)).to eq([1, 1]) + end + end + end + end + + context 'with max_count' do + it 'returns the number of commits up to the passed limit' do + options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + + expect(repository.count_commits(options)).to eq(10) + end end end @@ -1062,14 +1105,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "branch_names_contains" do - subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) } - - it { is_expected.to include('master') } - it { is_expected.not_to include('feature') } - it { is_expected.not_to include('fix') } - end - describe '#autocrlf' do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -1127,14 +1162,27 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding a branch' - it 'should reload Rugged::Repository and return master' do - expect(Rugged::Repository).to receive(:new).twice.and_call_original + context 'force_reload is true' do + it 'should reload Rugged::Repository' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original - repository.find_branch('master') - branch = repository.find_branch('master', force_reload: true) + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + end + + context 'force_reload is false' do + it 'should not reload Rugged::Repository' do + expect(Rugged::Repository).to receive(:new).once.and_call_original + + branch = repository.find_branch('master', force_reload: false) + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end end end end @@ -1241,48 +1289,58 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#merged_branch_names' do - context 'when branch names are passed' do - it 'only returns the names we are asking' do - names = repository.merged_branch_names(%w[merge-test]) + shared_examples 'finding merged branch names' do + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) - expect(names).to contain_exactly('merge-test') - end + expect(names).to contain_exactly('merge-test') + end - it 'does not return unmerged branch names' do - names = repository.merged_branch_names(%w[feature]) + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) - expect(names).to be_empty + expect(names).to be_empty + end end - end - context 'when no root ref is available' do - it 'returns empty list' do - project = create(:project, :empty_repo) + context 'when no root ref is available' do + it 'returns empty list' do + project = create(:project, :empty_repo) - names = project.repository.merged_branch_names(%w[feature]) + names = project.repository.merged_branch_names(%w[feature]) - expect(names).to be_empty + expect(names).to be_empty + end end - end - context 'when no branch names are specified' do - before do - repository.create_branch('identical', 'master') - end + context 'when no branch names are specified' do + before do + repository.create_branch('identical', 'master') + end - after do - ensure_seeds - end + after do + ensure_seeds + end - it 'returns all merged branch names except for identical one' do - names = repository.merged_branch_names + it 'returns all merged branch names except for identical one' do + names = repository.merged_branch_names - expect(names).to include('merge-test') - expect(names).to include('fix-mode') - expect(names).not_to include('feature') - expect(names).not_to include('identical') + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + expect(names).not_to include('identical') + end end end + + context 'when Gitaly merged_branch_names feature is enabled' do + it_behaves_like 'finding merged branch names' + end + + context 'when Gitaly merged_branch_names feature is disabled', :disable_gitaly do + it_behaves_like 'finding merged branch names' + end end describe "#ls_files" do @@ -1666,11 +1724,10 @@ describe Gitlab::Git::Repository, seed_helper: true do shared_examples "user deleting a branch" do let(:project) { create(:project, :repository) } let(:repository) { project.repository.raw } - let(:user) { create(:user) } let(:branch_name) { "to-be-deleted-soon" } before do - project.team << [user, :developer] + project.add_developer(user) repository.create_branch(branch_name) end @@ -1707,12 +1764,49 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#write_config' do + before do + repository.rugged.config["gitlab.fullpath"] = repository.path + end + + shared_examples 'writing repo config' do + context 'is given a path' do + it 'writes it to disk' do + repository.write_config(full_path: "not-the/real-path.git") + + config = File.read(File.join(repository.path, "config")) + + expect(config).to include("[gitlab]") + expect(config).to include("fullpath = not-the/real-path.git") + end + end + + context 'it is given an empty path' do + it 'does not write it to disk' do + repository.write_config(full_path: "") + + config = File.read(File.join(repository.path, "config")) + + expect(config).to include("[gitlab]") + expect(config).to include("fullpath = #{repository.path}") + end + end + end + + context "when gitaly_write_config is enabled" do + it_behaves_like "writing repo config" + end + + context "when gitaly_write_config is disabled", :disable_gitaly do + it_behaves_like "writing repo config" + end + end + describe '#merge' do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } - let(:user) { build(:user) } let(:target_branch) { 'test-merge-target-branch' } before do @@ -1734,6 +1828,20 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(result.repo_created).to eq(false) expect(result.branch_created).to eq(false) end + + it 'returns nil if there was a concurrent branch update' do + concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + result = repository.merge(user, source_sha, target_branch, 'Test merge') do + # This ref update should make the merge fail + repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) + end + + # This 'nil' signals that the merge was not applied + expect(result).to be_nil + + # Our concurrent ref update should not have been undone + expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id) + end end context 'with gitaly' do @@ -1751,7 +1859,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } - let(:user) { build(:user) } let(:target_branch) { 'test-ff-target-branch' } before do @@ -1843,6 +1950,276 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe 'remotes' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:remote_name) { 'my-remote' } + + after do + ensure_seeds + end + + describe '#add_remote' do + let(:url) { 'http://my-repo.git' } + let(:mirror_refmap) { '+refs/*:refs/*' } + + it 'creates a new remote via Gitaly' do + expect_any_instance_of(Gitlab::GitalyClient::RemoteService) + .to receive(:add_remote).with(remote_name, url, mirror_refmap) + + repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) + end + + context 'with Gitaly disabled', :skip_gitaly_mock do + it 'creates a new remote via Rugged' do + expect_any_instance_of(Rugged::RemoteCollection).to receive(:create) + .with(remote_name, url) + expect_any_instance_of(Rugged::Config).to receive(:[]=) + .with("remote.#{remote_name}.mirror", true) + expect_any_instance_of(Rugged::Config).to receive(:[]=) + .with("remote.#{remote_name}.prune", true) + expect_any_instance_of(Rugged::Config).to receive(:[]=) + .with("remote.#{remote_name}.fetch", mirror_refmap) + + repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) + end + end + end + + describe '#remove_remote' do + it 'removes the remote via Gitaly' do + expect_any_instance_of(Gitlab::GitalyClient::RemoteService) + .to receive(:remove_remote).with(remote_name) + + repository.remove_remote(remote_name) + end + + context 'with Gitaly disabled', :skip_gitaly_mock do + it 'removes the remote via Rugged' do + expect_any_instance_of(Rugged::RemoteCollection).to receive(:delete) + .with(remote_name) + + repository.remove_remote(remote_name) + end + end + end + end + + describe '#gitlab_projects' do + subject { repository.gitlab_projects } + + it { expect(subject.shard_path).to eq(storage_path) } + it { expect(subject.repository_relative_path).to eq(repository.relative_path) } + end + + describe '#bundle_to_disk' do + shared_examples 'bundling to disk' do + let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + + after do + FileUtils.rm_rf(save_path) + end + + it 'saves a bundle to disk' do + repository.bundle_to_disk(save_path) + + success = system( + *%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}), + [:out, :err] => '/dev/null' + ) + expect(success).to be true + end + end + + context 'when Gitaly bundle_to_disk feature is enabled' do + it_behaves_like 'bundling to disk' + end + + context 'when Gitaly bundle_to_disk feature is disabled', :disable_gitaly do + it_behaves_like 'bundling to disk' + end + end + + describe '#create_from_bundle' do + shared_examples 'creating repo from bundle' do + let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:project) { create(:project) } + let(:imported_repo) { project.repository.raw } + + before do + expect(repository.bundle_to_disk(bundle_path)).to be true + end + + after do + FileUtils.rm_rf(bundle_path) + end + + it 'creates a repo from a bundle file' do + expect(imported_repo).not_to exist + + result = imported_repo.create_from_bundle(bundle_path) + + expect(result).to be true + expect(imported_repo).to exist + expect { imported_repo.fsck }.not_to raise_exception + end + + it 'creates a symlink to the global hooks dir' do + imported_repo.create_from_bundle(bundle_path) + hooks_path = File.join(imported_repo.path, 'hooks') + + expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + end + + context 'when Gitaly create_repo_from_bundle feature is enabled' do + it_behaves_like 'creating repo from bundle' + end + + context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do + it_behaves_like 'creating repo from bundle' + end + end + + context 'gitlab_projects commands' do + let(:gitlab_projects) { repository.gitlab_projects } + let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } + + describe '#push_remote_branches' do + subject do + repository.push_remote_branches('downstream-remote', ['master']) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:push_branches) + .with('downstream-remote', timeout, true, ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'raises an error if the command fails' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:push_branches) + .with('downstream-remote', timeout, true, ['master']) + .and_return(false) + + expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') + end + end + + describe '#delete_remote_branches' do + subject do + repository.delete_remote_branches('downstream-remote', ['master']) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'raises an error if the command fails' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(false) + + expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') + end + end + + describe '#delete_remote_branches' do + subject do + repository.delete_remote_branches('downstream-remote', ['master']) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'raises an error if the command fails' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(false) + + expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') + end + end + + describe '#delete_remote_branches' do + subject do + repository.delete_remote_branches('downstream-remote', ['master']) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'raises an error if the command fails' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(false) + + expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') + end + end + + describe '#squash' do + let(:squash_id) { '1' } + let(:branch_name) { 'fix' } + let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' } + + subject do + opts = { + branch: branch_name, + start_sha: start_sha, + end_sha: end_sha, + author: user, + message: 'Squash commit message' + } + + repository.squash(user, squash_id, opts) + end + + context 'sparse checkout', :skip_gitaly_mock do + let(:expected_files) { %w(files files/js files/js/application.js) } + + before do + allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| + m.call(*args) do + worktree_path = args[0] + files_pattern = File.join(worktree_path, '**', '*') + expected = expected_files.map do |path| + File.expand_path(path, worktree_path) + end + + expect(Dir[files_pattern]).to eq(expected) + end + end + end + + it 'checkouts only the files in the diff' do + subject + end + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index eaf74951b0e..90fbef9d248 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -39,7 +39,7 @@ describe Gitlab::Git::RevList do ] expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:| - lazy_block.call(output.split("\n").lazy) + lazy_block.call(output.lines.lazy.map(&:chomp)) end end @@ -64,6 +64,15 @@ describe Gitlab::Git::RevList do expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) end + it 'can handle non utf-8 paths' do + non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") + stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") + + rev_list.new_objects(require_path: true) do |object_ids| + expect(object_ids.force).to eq(%w[sha2]) + end + end + it 'can yield a lazy enumerator' do stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 86f7bcb8e38..001e406a930 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -80,22 +80,18 @@ describe Gitlab::Git::Tree, seed_helper: true do end describe '#where' do - context 'with gitaly disabled' do - before do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) - end - - it 'calls #tree_entries_from_rugged' do - expect(described_class).to receive(:tree_entries_from_rugged) - - described_class.where(repository, SeedRepo::Commit::ID, '/') + shared_examples '#where' do + it 'returns an empty array when called with an invalid ref' do + expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([]) end end - it 'gets the tree entries from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::CommitService).to receive(:tree_entries) + context 'with gitaly' do + it_behaves_like '#where' + end - described_class.where(repository, SeedRepo::Commit::ID, '/') + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#where' end end end diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb new file mode 100644 index 00000000000..bd8dbf07fa7 --- /dev/null +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::Git::Wiki do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + let(:gollum_wiki) { wiki.wiki } + + # Remove skip_gitaly_mock flag when gitaly_find_page when + # https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged + describe '#page', :skip_gitaly_mock do + it 'returns the right page' do + create_page('page1', 'content') + create_page('foo/page1', 'content') + + expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1' + expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' + + destroy_page('page1') + destroy_page('page1', 'foo') + end + end + + def create_page(name, content) + gollum_wiki.write_page(name, :markdown, content, commit_details) + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + end + + def destroy_page(title, dir = '') + page = gollum_wiki.page(title: title, dir: dir) + wiki.delete_page(page, "test commit") + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 2db560c2cec..2009a8ac48c 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -51,12 +51,12 @@ describe Gitlab::GitAccess do context 'when the project exists' do context 'when actor exists' do context 'when actor is a DeployKey' do - let(:deploy_key) { create(:deploy_key, user: user, can_push: true) } + let(:deploy_key) { create(:deploy_key, user: user) } let(:actor) { deploy_key } context 'when the DeployKey has access to the project' do before do - deploy_key.projects << project + deploy_key.deploy_keys_projects.create(project: project, can_push: true) end it 'allows push and pull access' do @@ -275,7 +275,7 @@ describe Gitlab::GitAccess do describe '#check_command_disabled!' do before do - project.team << [user, :master] + project.add_master(user) end context 'over http' do @@ -404,7 +404,7 @@ describe Gitlab::GitAccess do describe 'reporter user' do before do - project.team << [user, :reporter] + project.add_reporter(user) end context 'pull code' do @@ -417,7 +417,7 @@ describe Gitlab::GitAccess do context 'when member of the project' do before do - project.team << [user, :reporter] + project.add_reporter(user) end context 'pull code' do @@ -497,7 +497,7 @@ describe Gitlab::GitAccess do if role == :admin user.update_attribute(:admin, true) else - project.team << [user, role] + project.add_role(user, role) end aggregate_failures do @@ -658,7 +658,7 @@ describe Gitlab::GitAccess do context 'when project is authorized' do before do - project.team << [user, :reporter] + project.add_reporter(user) end it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) } @@ -696,15 +696,13 @@ describe Gitlab::GitAccess do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key, user: user, can_push: can_push) } + let(:key) { create(:deploy_key, user: user) } let(:actor) { key } context 'when deploy_key can push' do - let(:can_push) { true } - context 'when project is authorized' do before do - key.projects << project + key.deploy_keys_projects.create(project: project, can_push: true) end it { expect { push_access_check }.not_to raise_error } @@ -732,11 +730,9 @@ describe Gitlab::GitAccess do end context 'when deploy_key cannot push' do - let(:can_push) { false } - context 'when project is authorized' do before do - key.projects << project + key.deploy_keys_projects.create(project: project, can_push: false) end it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 1056074264a..186b2d9279d 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki do context 'when user can :create_wiki' do before do create(:protected_branch, name: 'master', project: project) - project.team << [user, :developer] + project.add_developer(user) end subject { access.check('git-receive-pack', changes) } @@ -41,7 +41,7 @@ describe Gitlab::GitAccessWiki do subject { access.check('git-upload-pack', '_any') } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when wiki feature is enabled' do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index b2275119a04..3722a91c050 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -131,6 +131,29 @@ describe Gitlab::GitalyClient::CommitService do end end + describe '#commit_count' do + before do + expect_any_instance_of(Gitaly::CommitService::Stub) + .to receive(:count_commits) + .with(gitaly_request_with_path(storage_name, relative_path), + kind_of(Hash)) + .and_return([]) + end + + it 'sends a commit_count message' do + client.commit_count(revision) + end + + context 'with UTF-8 params strings' do + let(:revision) { "branch\u011F" } + let(:path) { "foo/\u011F.txt" } + + it 'handles string encodings correctly' do + client.commit_count(revision, path: path) + end + end + end + describe '#find_commit' do let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' } it 'sends an RPC request' do diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb new file mode 100644 index 00000000000..1c933410bd5 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::ConflictFilesStitcher do + describe 'enumeration' do + it 'combines segregated ConflictFile messages together' do + target_project = create(:project, :repository) + target_repository = target_project.repository.raw + target_gitaly_repository = target_repository.gitaly_repository + + our_path_1 = 'our/path/1' + their_path_1 = 'their/path/1' + our_mode_1 = 0744 + commit_oid_1 = 'f00' + content_1 = 'content of the first file' + + our_path_2 = 'our/path/2' + their_path_2 = 'their/path/2' + our_mode_2 = 0600 + commit_oid_2 = 'ba7' + content_2 = 'content of the second file' + + header_1 = double(repository: target_gitaly_repository, commit_oid: commit_oid_1, + our_path: our_path_1, their_path: their_path_1, our_mode: our_mode_1) + header_2 = double(repository: target_gitaly_repository, commit_oid: commit_oid_2, + our_path: our_path_2, their_path: their_path_2, our_mode: our_mode_2) + + messages = [ + double(files: [double(header: header_1), double(header: nil, content: content_1[0..5])]), + double(files: [double(header: nil, content: content_1[6..-1])]), + double(files: [double(header: header_2)]), + double(files: [double(header: nil, content: content_2[0..5]), double(header: nil, content: content_2[6..10])]), + double(files: [double(header: nil, content: content_2[11..-1])]) + ] + + conflict_files = described_class.new(messages).to_a + + expect(conflict_files.size).to be(2) + + expect(conflict_files[0].content).to eq(content_1) + expect(conflict_files[0].their_path).to eq(their_path_1) + expect(conflict_files[0].our_path).to eq(our_path_1) + expect(conflict_files[0].our_mode).to be(our_mode_1) + expect(conflict_files[0].repository).to eq(target_repository) + expect(conflict_files[0].commit_oid).to eq(commit_oid_1) + + expect(conflict_files[1].content).to eq(content_2) + expect(conflict_files[1].their_path).to eq(their_path_2) + expect(conflict_files[1].our_path).to eq(our_path_2) + expect(conflict_files[1].our_mode).to be(our_mode_2) + expect(conflict_files[1].repository).to eq(target_repository) + expect(conflict_files[1].commit_oid).to eq(commit_oid_2) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb new file mode 100644 index 00000000000..e4fe01a671f --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::ConflictsService do + let(:project) { create(:project, :repository) } + let(:target_project) { create(:project, :repository) } + let(:source_repository) { project.repository.raw } + let(:target_repository) { target_project.repository.raw } + let(:target_gitaly_repository) { target_repository.gitaly_repository } + let(:our_commit_oid) { 'f00' } + let(:their_commit_oid) { 'f44' } + let(:client) do + described_class.new(target_repository, our_commit_oid, their_commit_oid) + end + + describe '#list_conflict_files' do + let(:request) do + Gitaly::ListConflictFilesRequest.new( + repository: target_gitaly_repository, our_commit_oid: our_commit_oid, + their_commit_oid: their_commit_oid + ) + end + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:list_conflict_files) + .with(request, kind_of(Hash)).and_return([].to_enum) + + client.list_conflict_files + end + end + + describe '#resolve_conflicts' do + let(:user) { create(:user) } + let(:files) do + [{ old_path: 'some/path', new_path: 'some/path', content: '' }] + end + let(:source_branch) { 'master' } + let(:target_branch) { 'feature' } + let(:commit_message) { 'Solving conflicts' } + let(:resolution) do + Gitlab::Git::Conflict::Resolution.new(user, files, commit_message) + end + + subject do + client.resolve_conflicts(source_repository, resolution, source_branch, target_branch) + end + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:resolve_conflicts) + .with(kind_of(Enumerator), kind_of(Hash)).and_return(double(resolution_error: "")) + + subject + end + + it 'raises a relevant exception if resolution_error is present' do + expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:resolve_conflicts) + .with(kind_of(Enumerator), kind_of(Hash)).and_return(double(resolution_error: "something happened")) + + expect { subject }.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb new file mode 100644 index 00000000000..2c7e5eb5787 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::HealthCheckService do + let(:project) { create(:project) } + let(:storage_name) { project.repository_storage } + + subject { described_class.new(storage_name) } + + describe '#check' do + it 'successfully sends a health check request' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout).and_call_original + + expect(subject.check).to eq({ success: true }) + end + + it 'receives an unsuccessful health check request' do + expect_any_instance_of(Grpc::Health::V1::Health::Stub) + .to receive(:check) + .and_return(double(status: false)) + + expect(subject.check).to eq({ success: false }) + end + + it 'gracefully handles gRPC error' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout) + .and_raise(GRPC::Unavailable.new('Connection refused')) + + expect(subject.check).to eq({ success: false, message: '14:Connection refused' }) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index d9ec28ab02e..9fbdd73ee0e 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -123,4 +123,53 @@ describe Gitlab::GitalyClient::OperationService do expect(subject.branch_created).to be(false) end end + + describe '#user_squash' do + let(:branch_name) { 'my-branch' } + let(:squash_id) { '1' } + let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' } + let(:commit_message) { 'Squash message' } + let(:request) do + Gitaly::UserSquashRequest.new( + repository: repository.gitaly_repository, + user: gitaly_user, + squash_id: squash_id.to_s, + branch: branch_name, + start_sha: start_sha, + end_sha: end_sha, + author: gitaly_user, + commit_message: commit_message + ) + end + let(:squash_sha) { 'f00' } + let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) } + + subject do + client.user_squash(user, squash_id, branch_name, start_sha, end_sha, user, commit_message) + end + + it 'sends a user_squash message and returns the squash sha' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_squash).with(request, kind_of(Hash)) + .and_return(response) + + expect(subject).to eq(squash_sha) + end + + context "when git_error is present" do + let(:response) do + Gitaly::UserSquashResponse.new(git_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_squash).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::Repository::GitError, "something failed") + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 951e146a30a..257e4c50f2d 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -112,7 +112,7 @@ describe Gitlab::GitalyClient::RefService do expect_any_instance_of(Gitaly::RefService::Stub) .to receive(:delete_refs) .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) - .and_return(double('delete_refs_response')) + .and_return(double('delete_refs_response', git_error: "")) client.delete_refs(except_with_prefixes: prefixes) end diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb new file mode 100644 index 00000000000..872377c93d8 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::RemoteService do + let(:project) { create(:project) } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.disk_path + '.git' } + let(:remote_name) { 'my-remote' } + let(:client) { described_class.new(project.repository) } + + describe '#add_remote' do + let(:url) { 'http://my-repo.git' } + let(:mirror_refmap) { :all_refs } + + it 'sends an add_remote message' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:add_remote) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(:add_remote_response)) + + client.add_remote(remote_name, url, mirror_refmap) + end + end + + describe '#remove_remote' do + it 'sends an remove_remote message and returns the result value' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:remove_remote) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(result: true)) + + expect(client.remove_remote(remote_name)).to be(true) + end + end + + describe '#fetch_internal_remote' do + let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + it 'sends an fetch_internal_remote message and returns the result value' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:fetch_internal_remote) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(result: true)) + + expect(client.fetch_internal_remote(remote_repository)).to be(true) + end + end + + describe '#update_remote_mirror' do + let(:ref_name) { 'remote_mirror_1' } + let(:only_branches_matching) { ['my-branch', 'master'] } + + it 'sends an update_remote_mirror message' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:update_remote_mirror) + .with(kind_of(Enumerator), kind_of(Hash)) + .and_return(double(:update_remote_mirror_response)) + + client.update_remote_mirror(ref_name, only_branches_matching) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index a871ed0df0e..81bcd8c28ed 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -3,6 +3,31 @@ require 'spec_helper' # We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want # those stubs while testing the GitalyClient itself. describe Gitlab::GitalyClient, skip_gitaly_mock: true do + describe '.stub_class' do + it 'returns the gRPC health check stub' do + expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) + end + + it 'returns a Gitaly stub' do + expect(described_class.stub_class(:ref_service)).to eq(::Gitaly::RefService::Stub) + end + end + + describe '.stub_address' do + it 'returns the same result after being called multiple times' do + address = 'localhost:9876' + prefixed_address = "tcp://#{address}" + + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'default' => { 'gitaly_address' => prefixed_address } + }) + + 2.times do + expect(described_class.stub_address('default')).to eq('localhost:9876') + end + end + end + describe '.stub' do # Notice that this is referring to gRPC "stubs", not rspec stubs before do @@ -38,20 +63,6 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end - describe 'encode' do - [ - [nil, ""], - ["", ""], - [" ", " "], - %w(a1 a1), - ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] - ].each do |input, result| - it "encodes #{input.inspect} to #{result.inspect}" do - expect(described_class.encode(input)).to eq result - end - end - end - describe 'allow_n_plus_1_calls' do context 'when RequestStore is enabled', :request_store do it 'returns the result of the allow_n_plus_1_calls block' do diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index d72572cd510..44695acbe7d 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -244,7 +244,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do it 'returns true when a commit exists' do expect(project.repository) - .to receive(:lookup) + .to receive(:commit) .with('123') .and_return(double(:commit)) @@ -253,9 +253,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do it 'returns false when a commit does not exist' do expect(project.repository) - .to receive(:lookup) + .to receive(:commit) .with('123') - .and_raise(Rugged::OdbError) + .and_return(nil) expect(importer.commit_exists?('123')).to eq(false) end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index 798ea0bac58..017facd0f5e 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer do subject { described_class.new(project) } before do - project.team << [project.creator, :master] + project.add_master(project.creator) project.create_import_data(data: import_data) end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index a6c99bc07d4..e3bf2801406 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -38,8 +38,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -77,8 +77,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User3.signed_commit_signature, @@ -116,8 +116,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -151,8 +151,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -187,8 +187,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -217,8 +217,8 @@ describe Gitlab::Gpg::Commit do let!(:commit) { create :commit, project: project, sha: commit_sha } before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index d6000af0ecd..c034eccf2a6 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -26,8 +26,8 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do before do allow_any_instance_of(Project).to receive(:commit).and_return(commit) - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return(signature) end diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb new file mode 100644 index 00000000000..724beefff69 --- /dev/null +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::HealthChecks::GitalyCheck do + let(:result_class) { Gitlab::HealthChecks::Result } + let(:repository_storages) { ['default'] } + + before do + allow(described_class).to receive(:repository_storages) { repository_storages } + end + + describe '#readiness' do + subject { described_class.readiness } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) } + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) } + end + end + + describe '#metrics' do + subject { described_class.metrics } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: 'default' })) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 1)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it 'provides metrics' do + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + end +end diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index aeacd577d18..506b2c0be20 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -14,7 +14,6 @@ describe Gitlab::HookData::IssueBuilder do closed_at confidential created_at - deleted_at description due_date id diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 78475403f9e..b61614e4790 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -12,7 +12,6 @@ describe Gitlab::HookData::MergeRequestBuilder do assignee_id author_id created_at - deleted_at description head_pipeline_id id diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 162b776e107..5cdc5138fda 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -12,30 +12,61 @@ describe Gitlab::ImportExport::FileImporter do stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) - + allow(SecureRandom).to receive(:hex).and_return('abcd') setup_files - - described_class.import(archive_file: '', shared: shared) end after do FileUtils.rm_rf(export_path) end - it 'removes symlinks in root folder' do - expect(File.exist?(symlink_file)).to be false - end + context 'normal run' do + before do + described_class.import(archive_file: '', shared: shared) + end - it 'removes hidden symlinks in root folder' do - expect(File.exist?(hidden_symlink_file)).to be false - end + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end - it 'removes symlinks in subfolders' do - expect(File.exist?(subfolder_symlink_file)).to be false + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + + it 'creates the file in the right subfolder' do + expect(shared.export_path).to include('test/abcd') + end end - it 'does not remove a valid file' do - expect(File.exist?(valid_file)).to be true + context 'error' do + before do + allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) + described_class.import(archive_file: '', shared: shared) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end end def setup_files diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json index 82a1fbd2fc5..1a561e81e4a 100644 --- a/spec/lib/gitlab/import_export/project.group.json +++ b/spec/lib/gitlab/import_export/project.group.json @@ -54,7 +54,6 @@ "iid": 1, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, @@ -134,7 +133,6 @@ "iid": 2, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index f0752649121..b6c1f0c81cb 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -56,7 +56,6 @@ "iid": 10, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "test_ee_field": "test", @@ -350,7 +349,6 @@ "iid": 9, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "milestone": { @@ -586,7 +584,6 @@ "iid": 8, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "label_links": [ @@ -820,7 +817,6 @@ "iid": 7, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1033,7 +1029,6 @@ "iid": 6, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1246,7 +1241,6 @@ "iid": 5, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1459,7 +1453,6 @@ "iid": 4, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1672,7 +1665,6 @@ "iid": 3, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1885,7 +1877,6 @@ "iid": 2, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -2098,7 +2089,6 @@ "iid": 1, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -2504,7 +2494,6 @@ "merge_when_pipeline_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 671, @@ -2948,7 +2937,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 679, @@ -3228,7 +3216,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 777, @@ -3508,7 +3495,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 785, @@ -4198,7 +4184,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 793, @@ -4734,7 +4719,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 801, @@ -5223,7 +5207,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 809, @@ -5478,7 +5461,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 817, @@ -6168,7 +6150,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 825, @@ -6465,78 +6446,100 @@ } } ], - "statuses": [ - { - "id": 71, - "project_id": 5, - "status": "failed", - "finished_at": "2016-03-29T06:28:12.630Z", - "trace": null, - "created_at": "2016-03-22T15:20:35.772Z", - "updated_at": "2016-03-29T06:28:12.634Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 36, - "commands": "$ build command", - "job_id": null, - "name": "test build 1", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, - "erased_by_id": null, - "erased_at": null, - "type": "Ci::Build", - "token": "abcd" - }, - { - "id": 72, - "project_id": 5, - "status": "success", - "finished_at": null, - "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.", - "created_at": "2016-03-22T15:20:35.777Z", - "updated_at": "2016-03-22T15:20:35.777Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 36, - "commands": "$ build command", - "job_id": null, - "name": "test build 2", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null + "stages": [ + { + "id": 11, + "project_id": 5, + "pipeline_id": 36, + "name": "test", + "status": 1, + "created_at": "2016-03-22T15:44:44.772Z", + "updated_at": "2016-03-29T06:44:44.634Z", + "statuses": [ + { + "id": 71, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.630Z", + "trace": null, + "created_at": "2016-03-22T15:20:35.772Z", + "updated_at": "2016-03-29T06:28:12.634Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "stage_id": 11, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null, + "type": "Ci::Build", + "token": "abcd" + }, + { + "id": 72, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.", + "created_at": "2016-03-22T15:20:35.777Z", + "updated_at": "2016-03-22T15:20:35.777Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ deploy command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "deploy", + "trigger_request_id": null, + "stage_idx": 1, + "stage_id": 12, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 12, + "project_id": 5, + "pipeline_id": 36, + "name": "deploy", + "status": 2, + "created_at": "2016-03-22T15:45:45.772Z", + "updated_at": "2016-03-29T06:45:45.634Z" } ] }, @@ -6556,76 +6559,87 @@ "started_at": null, "finished_at": null, "duration": null, - "statuses": [ - { - "id": 74, - "project_id": 5, - "status": "success", - "finished_at": null, - "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.", - "created_at": "2016-03-22T15:20:35.846Z", - "updated_at": "2016-03-22T15:20:35.846Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 37, - "commands": "$ build command", - "job_id": null, - "name": "test build 2", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null - }, - { - "id": 73, - "project_id": 5, - "status": "canceled", - "finished_at": null, - "trace": null, - "created_at": "2016-03-22T15:20:35.842Z", - "updated_at": "2016-03-22T15:20:35.842Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 37, - "commands": "$ build command", - "job_id": null, - "name": "test build 1", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, - "erased_by_id": null, - "erased_at": null + "stages": [ + { + "id": 21, + "project_id": 5, + "pipeline_id": 37, + "name": "test", + "status": 1, + "created_at": "2016-03-22T15:44:44.772Z", + "updated_at": "2016-03-29T06:44:44.634Z", + "statuses": [ + { + "id": 74, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.", + "created_at": "2016-03-22T15:20:35.846Z", + "updated_at": "2016-03-22T15:20:35.846Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 73, + "project_id": 5, + "status": "canceled", + "finished_at": null, + "trace": null, + "created_at": "2016-03-22T15:20:35.842Z", + "updated_at": "2016-03-22T15:20:35.842Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + } + ] } ] }, @@ -6645,76 +6659,87 @@ "started_at": null, "finished_at": null, "duration": null, - "statuses": [ - { - "id": 76, - "project_id": 5, - "status": "success", - "finished_at": null, - "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.", - "created_at": "2016-03-22T15:20:35.882Z", - "updated_at": "2016-03-22T15:20:35.882Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 38, - "commands": "$ build command", - "job_id": null, - "name": "test build 2", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null - }, - { - "id": 75, - "project_id": 5, - "status": "failed", - "finished_at": null, - "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.", - "created_at": "2016-03-22T15:20:35.864Z", - "updated_at": "2016-03-22T15:20:35.864Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 38, - "commands": "$ build command", - "job_id": null, - "name": "test build 1", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null + "stages": [ + { + "id": 22, + "project_id": 5, + "pipeline_id": 38, + "name": "test", + "status": 1, + "created_at": "2016-03-22T15:44:44.772Z", + "updated_at": "2016-03-29T06:44:44.634Z", + "statuses": [ + { + "id": 76, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.", + "created_at": "2016-03-22T15:20:35.882Z", + "updated_at": "2016-03-22T15:20:35.882Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 75, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.", + "created_at": "2016-03-22T15:20:35.864Z", + "updated_at": "2016-03-22T15:20:35.864Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] } ] }, @@ -6734,76 +6759,87 @@ "started_at": null, "finished_at": null, "duration": null, - "statuses": [ - { - "id": 78, - "project_id": 5, - "status": "success", - "finished_at": null, - "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.", - "created_at": "2016-03-22T15:20:35.927Z", - "updated_at": "2016-03-22T15:20:35.927Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 39, - "commands": "$ build command", - "job_id": null, - "name": "test build 2", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null - }, - { - "id": 77, - "project_id": 5, - "status": "failed", - "finished_at": null, - "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.", - "created_at": "2016-03-22T15:20:35.905Z", - "updated_at": "2016-03-22T15:20:35.905Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 39, - "commands": "$ build command", - "job_id": null, - "name": "test build 1", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null + "stages": [ + { + "id": 23, + "project_id": 5, + "pipeline_id": 39, + "name": "test", + "status": 1, + "created_at": "2016-03-22T15:44:44.772Z", + "updated_at": "2016-03-29T06:44:44.634Z", + "statuses": [ + { + "id": 78, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.", + "created_at": "2016-03-22T15:20:35.927Z", + "updated_at": "2016-03-22T15:20:35.927Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 77, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.", + "created_at": "2016-03-22T15:20:35.905Z", + "updated_at": "2016-03-22T15:20:35.905Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] } ] }, @@ -6823,76 +6859,87 @@ "started_at": null, "finished_at": null, "duration": null, - "statuses": [ - { - "id": 79, - "project_id": 5, - "status": "failed", - "finished_at": "2016-03-29T06:28:12.695Z", - "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.", - "created_at": "2016-03-22T15:20:35.950Z", - "updated_at": "2016-03-29T06:28:12.696Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 40, - "commands": "$ build command", - "job_id": null, - "name": "test build 1", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, - "erased_by_id": null, - "erased_at": null - }, - { - "id": 80, - "project_id": 5, - "status": "success", - "finished_at": null, - "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.", - "created_at": "2016-03-22T15:20:35.966Z", - "updated_at": "2016-03-22T15:20:35.966Z", - "started_at": null, - "runner_id": null, - "coverage": null, - "commit_id": 40, - "commands": "$ build command", - "job_id": null, - "name": "test build 2", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": "test", - "trigger_request_id": null, - "stage_idx": 1, - "tag": null, - "ref": "master", - "user_id": null, - "target_url": null, - "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" - }, - "erased_by_id": null, - "erased_at": null + "stages": [ + { + "id": 24, + "project_id": 5, + "pipeline_id": 40, + "name": "test", + "status": 1, + "created_at": "2016-03-22T15:44:44.772Z", + "updated_at": "2016-03-29T06:44:44.634Z", + "statuses": [ + { + "id": 79, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.695Z", + "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.", + "created_at": "2016-03-22T15:20:35.950Z", + "updated_at": "2016-03-29T06:28:12.696Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 40, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 80, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.", + "created_at": "2016-03-22T15:20:35.966Z", + "updated_at": "2016-03-22T15:20:35.966Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 40, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" + }, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] } ] } @@ -6902,7 +6949,6 @@ "id": 123, "token": "cdbfasdf44a5958c83654733449e585", "project_id": 5, - "deleted_at": null, "created_at": "2017-01-16T15:25:28.637Z", "updated_at": "2017-01-16T15:25:28.637Z" } @@ -7050,7 +7096,7 @@ "project_id": 5, "created_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z", - "active": false, + "active": true, "properties": { }, diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index 02450478a77..5dbf0ed289b 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -54,7 +54,6 @@ "iid": 20, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, 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 0ab3afd0074..d076007e4bc 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -179,6 +179,32 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end end + + context 'when restoring hierarchy of pipeline, stages and jobs' do + it 'restores pipelines' do + expect(Ci::Pipeline.all.count).to be 5 + end + + it 'restores pipeline stages' do + expect(Ci::Stage.all.count).to be 6 + end + + it 'correctly restores association between stage and a pipeline' do + expect(Ci::Stage.all).to all(have_attributes(pipeline_id: a_value > 0)) + end + + it 'restores statuses' do + expect(CommitStatus.all.count).to be 10 + end + + it 'correctly restores association between a stage and a job' do + expect(CommitStatus.all).to all(have_attributes(stage_id: a_value > 0)) + end + + it 'correctly restores association between a pipeline and a job' do + expect(CommitStatus.all).to all(have_attributes(pipeline_id: a_value > 0)) + end + end end end @@ -210,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do labels = project.issues.first.labels expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) + expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0) end end shared_examples 'restores group correctly' do |**results| it 'has group label' do expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) + expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0) end it 'has group milestone' do diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 6243b6ac9f0..5804c45871e 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do let!(:project) { setup_project } before do - project.team << [user, :master] + project.add_master(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD') allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA') @@ -109,12 +109,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty end + it 'has pipeline stages' do + expect(saved_project_json.dig('pipelines', 0, 'stages')).not_to be_empty + end + it 'has pipeline statuses' do - expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty + expect(saved_project_json.dig('pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty end it 'has pipeline builds' do - expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1) + builds_count = saved_project_json + .dig('pipelines', 0, 'stages', 0, 'statuses') + .count { |hash| hash['type'] == 'Ci::Build' } + + expect(builds_count).to eq(1) end it 'has no when YML attributes but only the DB column' do @@ -156,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end + it 'saves the properties for a service' do + expect(saved_project_json['services'].first['properties']).to eq('one' => 'value') + end + it 'has project feature' do project_feature = saved_project_json['project_feature'] expect(project_feature).not_to be_empty @@ -271,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do commit_id: ci_build.pipeline.sha) create(:event, :created, target: milestone, project: project, author: user) - create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') + create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index e6ad516deef..44f972fe530 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RepoSaver do let(:bundler) { described_class.new(project: project, shared: shared) } before do - project.team << [user, :master] + project.add_master(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ec8fa99e0da..5a33fa3fd53 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -14,7 +14,6 @@ Issue: - iid - updated_by_id - confidential -- deleted_at - closed_at - due_date - moved_to_id @@ -159,7 +158,6 @@ MergeRequest: - merge_when_pipeline_succeeds - merge_user_id - merge_commit_sha -- deleted_at - in_progress_merge_commit_sha - lock_version - milestone_id @@ -180,6 +178,7 @@ MergeRequestDiff: - real_size - head_commit_sha - start_commit_sha +- commits_count MergeRequestDiffCommit: - merge_request_diff_id - relative_order @@ -293,7 +292,6 @@ Ci::Trigger: - id - token - project_id -- deleted_at - created_at - updated_at - owner_id @@ -309,7 +307,6 @@ Ci::PipelineSchedule: - project_id - owner_id - active -- deleted_at - created_at - updated_at Clusters::Cluster: @@ -459,6 +456,7 @@ Project: - delete_error - merge_requests_ff_only_enabled - merge_requests_rebase_enabled +- jobs_cache_index Author: - name ProjectFeature: diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb index 63992ea8ab8..a685521cbf0 100644 --- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb @@ -4,7 +4,6 @@ describe Gitlab::ImportExport::UploadsRestorer do describe 'bundle a project Git repo' do let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } - let(:uploads_path) { FileUploader.dynamic_path_segment(project) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -26,9 +25,9 @@ describe Gitlab::ImportExport::UploadsRestorer do end it 'copies the uploads to the project path' do - restorer.restore + subject.restore - uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) } expect(uploads).to include('dummy.txt') end @@ -44,9 +43,9 @@ describe Gitlab::ImportExport::UploadsRestorer do end it 'copies the uploads to the project path' do - restorer.restore + subject.restore - uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) } expect(uploads).to include('dummy.txt') end diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index e8948de1f3a..959779523f4 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::ImportExport::UploadsSaver do it 'copies the uploads to the export path' do saver.save - uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) } expect(uploads).to include('banana_sample.gif') end @@ -52,7 +52,7 @@ describe Gitlab::ImportExport::UploadsSaver do it 'copies the uploads to the export path' do saver.save - uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) } expect(uploads).to include('banana_sample.gif') end diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 0e55993c8ef..1d1e7e7f89a 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do let!(:project_wiki) { ProjectWiki.new(project, user) } before do - project.team << [user, :master] + project.add_master(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) project_wiki.wiki project_wiki.create_page("index", "test content") diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb new file mode 100644 index 00000000000..6532579b1c9 --- /dev/null +++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::InsecureKeyFingerprint do + let(:key) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn' \ + '1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qk' \ + 'r8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMg' \ + 'Jw0=' + end + + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(described_class.new(key.split[1]).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 15f99b0401f..69112fe90b1 100644 --- a/spec/lib/gitlab/kubernetes/helm_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -1,22 +1,23 @@ require 'spec_helper' -describe Gitlab::Kubernetes::Helm do +describe Gitlab::Kubernetes::Helm::Api do let(:client) { double('kubernetes client') } let(:helm) { described_class.new(client) } - let(:namespace) { Gitlab::Kubernetes::Namespace.new(described_class::NAMESPACE, client) } + let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } let(:install_helm) { true } let(:chart) { 'stable/a_chart' } let(:application_name) { 'app_name' } - let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm, chart) } + let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) } subject { helm } before do - allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client).and_return(namespace) + allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace) end describe '#initialize' do it 'creates a namespace object' do - expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client) + expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client) subject end @@ -41,7 +42,7 @@ describe Gitlab::Kubernetes::Helm do let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation it 'fetches POD phase from kubernetes cluster' do - expect(client).to receive(:get_pod).with(command.pod_name, described_class::NAMESPACE).once.and_return(pod) + expect(client).to receive(:get_pod).with(command.pod_name, gitlab_namespace).once.and_return(pod) expect(subject.installation_status(command.pod_name)).to eq(phase) end @@ -52,7 +53,7 @@ describe Gitlab::Kubernetes::Helm do let(:response) { RestClient::Response.new(log) } it 'fetches POD phase from kubernetes cluster' do - expect(client).to receive(:get_pod_log).with(command.pod_name, described_class::NAMESPACE).once.and_return(response) + expect(client).to receive(:get_pod_log).with(command.pod_name, gitlab_namespace).once.and_return(response) expect(subject.installation_log(command.pod_name)).to eq(log) end @@ -60,41 +61,9 @@ describe Gitlab::Kubernetes::Helm do describe '#delete_installation_pod!' do it 'deletes the POD from kubernetes cluster' do - expect(client).to receive(:delete_pod).with(command.pod_name, described_class::NAMESPACE).once + expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once subject.delete_installation_pod!(command.pod_name) end end - - describe '#helm_init_command' do - subject { helm.send(:helm_init_command, command) } - - context 'when command.install_helm is true' do - let(:install_helm) { true } - - it { is_expected.to eq('helm init >/dev/null') } - end - - context 'when command.install_helm is false' do - let(:install_helm) { false } - - it { is_expected.to eq('helm init --client-only >/dev/null') } - end - end - - describe '#helm_install_command' do - subject { helm.send(:helm_install_command, command) } - - context 'when command.chart is nil' do - let(:chart) { nil } - - it { is_expected.to be_nil } - end - - context 'when command.chart is set' do - let(:chart) { 'stable/a_chart' } - - it { is_expected.to eq("helm install #{chart} --name #{application_name} --namespace #{namespace.name} >/dev/null")} - end - end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb new file mode 100644 index 00000000000..63997a40d52 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -0,0 +1,130 @@ +require 'rails_helper' + +describe Gitlab::Kubernetes::Helm::InstallCommand do + let(:prometheus) { create(:clusters_applications_prometheus) } + + describe "#initialize" do + context "With all the params" do + subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } + + it 'should assign all parameters' do + expect(subject.name).to eq(prometheus.name) + expect(subject.install_helm).to be_truthy + expect(subject.chart).to eq(prometheus.chart) + expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml") + end + end + + context 'when install_helm is not set' do + subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) } + + it 'should set install_helm as false' do + expect(subject.install_helm).to be_falsy + end + end + + context 'when chart is not set' do + subject { described_class.new(prometheus.name, install_helm: true) } + + it 'should set chart as nil' do + expect(subject.chart).to be_falsy + end + end + + context 'when chart_values_file is not set' do + subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) } + + it 'should set chart_values_file as nil' do + expect(subject.chart_values_file).to be_falsy + end + end + end + + describe "#generate_script" do + let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) } + let(:client) { double('kubernetes client') } + let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } + subject { install_command.send(:generate_script, namespace.name) } + + context 'when install helm is true' do + let(:install_helm) { true } + let(:command) do + <<~MSG + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + + helm init >/dev/null + MSG + end + + it 'should return appropriate command' do + is_expected.to eq(command) + end + end + + context 'when install helm is false' do + let(:install_helm) { false } + let(:command) do + <<~MSG + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + + helm init --client-only >/dev/null + MSG + end + + it 'should return appropriate command' do + is_expected.to eq(command) + end + end + + context 'when chart is present' do + let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) } + let(:command) do + <<~MSG.chomp + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + + helm init --client-only >/dev/null + helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null + MSG + end + + it 'should return appropriate command' do + is_expected.to eq(command) + end + end + + context 'when chart values file is present' do + let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } + let(:command) do + <<~MSG.chomp + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + + helm init --client-only >/dev/null + helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} -f /data/helm/#{prometheus.name}/config/values.yaml >/dev/null + MSG + end + + it 'should return appropriate command' do + is_expected.to eq(command) + end + end + end + + describe "#pod_name" do + let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) } + subject { install_command.send(:pod_name) } + + it { is_expected.to eq('install-prometheus') } + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb new file mode 100644 index 00000000000..ebb6033f71e --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +describe Gitlab::Kubernetes::Helm::Pod do + describe '#generate' do + let(:cluster) { create(:cluster) } + let(:app) { create(:clusters_applications_prometheus, cluster: cluster) } + let(:command) { app.install_command } + let(:client) { double('kubernetes client') } + let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } + subject { described_class.new(command, namespace.name, client) } + + before do + allow(client).to receive(:create_config_map).and_return(nil) + end + + shared_examples 'helm pod' do + it 'should generate a Kubeclient::Resource' do + expect(subject.generate).to be_a_kind_of(Kubeclient::Resource) + end + + it 'should generate the appropriate metadata' do + metadata = subject.generate.metadata + expect(metadata.name).to eq("install-#{app.name}") + expect(metadata.namespace).to eq('gitlab-managed-apps') + expect(metadata.labels['gitlab.org/action']).to eq('install') + expect(metadata.labels['gitlab.org/application']).to eq(app.name) + end + + it 'should generate a container spec' do + spec = subject.generate.spec + expect(spec.containers.count).to eq(1) + end + + it 'should generate the appropriate specifications for the container' do + container = subject.generate.spec.containers.first + expect(container.name).to eq('helm') + expect(container.image).to eq('alpine:3.6') + expect(container.env.count).to eq(3) + expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) + expect(container.command).to match_array(["/bin/sh"]) + expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"]) + end + + it 'should include a never restart policy' do + spec = subject.generate.spec + expect(spec.restartPolicy).to eq('Never') + end + end + + context 'with a configuration file' do + it_behaves_like 'helm pod' + + it 'should include volumes for the container' do + container = subject.generate.spec.containers.first + expect(container.volumeMounts.first['name']).to eq('configuration-volume') + expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config") + end + + it 'should include a volume inside the specification' do + spec = subject.generate.spec + expect(spec.volumes.first['name']).to eq('configuration-volume') + end + + it 'should mount configMap specification in the volume' do + spec = subject.generate.spec + expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}") + expect(spec.volumes.first.configMap['items'].first['key']).to eq('values') + expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml') + end + end + + context 'without a configuration file' do + let(:app) { create(:clusters_applications_helm, cluster: cluster) } + + it_behaves_like 'helm pod' + + it 'should not include volumeMounts inside the container' do + container = subject.generate.spec.containers.first + expect(container.volumeMounts).to be_nil + end + + it 'should not a volume inside the specification' do + spec = subject.generate.spec + expect(spec.volumes).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index d9ddb4326be..6132abd9b35 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search) do |arg| expect(arg[:filter].to_s).to eq('(uid=johndoe)') expect(arg[:base]).to eq('dc=example,dc=com') - expect(arg[:attributes]).to match(%w{dn uid cn mail email userPrincipalName}) + expect(arg[:attributes]).to match(ldap_attributes) end.and_return({}) adapter.users('uid', 'johndoe') @@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search).with( base: 'uid=johndoe,ou=users,dc=example,dc=com', scope: Net::LDAP::SearchScope_BaseObject, - attributes: %w{dn uid cn mail email userPrincipalName}, + attributes: ldap_attributes, filter: nil ).and_return({}) @@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter do it 'uses the right uid attribute when non-default' do stub_ldap_config(uid: 'sAMAccountName') expect(adapter).to receive(:ldap_search).with( - hash_including(attributes: %w{dn sAMAccountName cn mail email userPrincipalName}) + hash_including(attributes: ldap_attributes) ).and_return({}) adapter.users('sAMAccountName', 'johndoe') @@ -137,4 +137,8 @@ describe Gitlab::LDAP::Adapter do end end end + + def ldap_attributes + Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain')) + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index d204050ef66..ff29d9aa5be 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -8,13 +8,16 @@ describe Gitlab::LDAP::Person do before do stub_ldap_config( options: { + 'uid' => 'uid', 'attributes' => { - 'name' => 'cn', - 'email' => %w(mail email userPrincipalName) + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName), + 'username' => username_attribute } } ) end + let(:username_attribute) { %w(uid sAMAccountName userid) } describe '.normalize_dn' do subject { described_class.normalize_dn(given) } @@ -44,6 +47,34 @@ describe Gitlab::LDAP::Person do end end + describe '.ldap_attributes' do + it 'returns a compact and unique array' do + stub_ldap_config( + options: { + 'uid' => nil, + 'attributes' => { + 'name' => 'cn', + 'email' => 'mail', + 'username' => %w(uid mail memberof) + } + } + ) + config = Gitlab::LDAP::Config.new('ldapmain') + ldap_attributes = described_class.ldap_attributes(config) + + expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof)) + end + end + + describe '.validate_entry' do + it 'raises InvalidEntryError' do + entry['foo'] = 'bar' + + expect { described_class.new(entry, 'ldapmain') } + .to raise_error(Gitlab::LDAP::Person::InvalidEntryError) + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' @@ -72,6 +103,44 @@ describe Gitlab::LDAP::Person do end end + describe '#username' do + context 'with default uid username attribute' do + let(:username_attribute) { 'uid' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a different username attribute' do + let(:username_attribute) { 'sAMAccountName' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a non-standard username attribute' do + let(:username_attribute) { 'mail' } + + it 'returns the proper username value' do + attr_value = 'john.doe@example.com' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + end + def assert_generic_test(test_description, got, expected) test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" expect(got).to eq(expected), test_failure_message diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index 41a9d1d9c90..d9379cfe674 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -5,6 +5,10 @@ describe Gitlab::Metrics::MethodCall do let(:method_call) { described_class.new('Foo#bar', :Foo, '#bar', transaction) } describe '#measure' do + after do + described_class.reload_metric!(:gitlab_method_call_duration_seconds) + end + it 'measures the performance of the supplied block' do method_call.measure { 'foo' } @@ -20,8 +24,6 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is enabled' do before do - allow(Feature.get(:prometheus_metrics_method_instrumentation)).to receive(:enabled?).and_call_original - described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 Feature.get(:prometheus_metrics_method_instrumentation).enable end @@ -31,30 +33,12 @@ describe Gitlab::Metrics::MethodCall do end end - it 'caches subsequent invocations of feature check' do - 10.times do - method_call.measure { 'foo' } - end - - expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).once - end - - it 'expires feature check cache after 1 minute' do - method_call.measure { 'foo' } - - Timecop.travel(1.minute.from_now) do - method_call.measure { 'foo' } - end - - Timecop.travel(1.minute.from_now + 1.second) do - method_call.measure { 'foo' } - end - - expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).twice + it 'metric is not a NullMetric' do + expect(described_class).not_to be_instance_of(Gitlab::Metrics::NullMetric) end it 'observes the performance of the supplied block' do - expect(described_class.call_duration_histogram) + expect(described_class.gitlab_method_call_duration_seconds) .to receive(:observe) .with({ module: :Foo, method: '#bar' }, be_a_kind_of(Numeric)) @@ -64,14 +48,12 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is disabled' do before do - described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 - Feature.get(:prometheus_metrics_method_instrumentation).disable end - it 'does not observe the performance' do - expect(described_class.call_duration_histogram) - .not_to receive(:observe) + it 'observes using NullMetric' do + expect(described_class.gitlab_method_call_duration_seconds).to be_instance_of(Gitlab::Metrics::NullMetric) + expect(described_class.gitlab_method_call_duration_seconds).to receive(:observe) method_call.measure { 'foo' } end @@ -81,12 +63,10 @@ describe Gitlab::Metrics::MethodCall do context 'when measurement is below threshold' do before do allow(method_call).to receive(:above_threshold?).and_return(false) - - Feature.get(:prometheus_metrics_method_instrumentation).enable end it 'does not observe the performance' do - expect(described_class.call_duration_histogram) + expect(described_class.gitlab_method_call_duration_seconds) .not_to receive(:observe) method_call.measure { 'foo' } @@ -96,7 +76,7 @@ describe Gitlab::Metrics::MethodCall do describe '#to_metric' do it 'returns a Metric instance' do - expect(method_call).to receive(:real_time).and_return(4.0001) + expect(method_call).to receive(:real_time).and_return(4.0001).twice expect(method_call).to receive(:cpu_time).and_return(3.0001) method_call.measure { 'foo' } diff --git a/spec/lib/gitlab/metrics/methods_spec.rb b/spec/lib/gitlab/metrics/methods_spec.rb new file mode 100644 index 00000000000..9d41ed2442b --- /dev/null +++ b/spec/lib/gitlab/metrics/methods_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Methods do + subject { Class.new { include Gitlab::Metrics::Methods } } + + shared_context 'metric' do |metric_type, *args| + let(:docstring) { 'description' } + let(:metric_name) { :sample_metric } + + describe "#define_#{metric_type}" do + define_method(:call_define_metric_method) do |**args| + subject.__send__("define_#{metric_type}", metric_name, **args) + end + + context 'metrics access method not defined' do + it "defines metrics accessing method" do + expect(subject).not_to respond_to(metric_name) + + call_define_metric_method(docstring: docstring) + + expect(subject).to respond_to(metric_name) + end + end + + context 'metrics access method defined' do + before do + call_define_metric_method(docstring: docstring) + end + + it 'raises error when trying to redefine method' do + expect { call_define_metric_method(docstring: docstring) }.to raise_error(ArgumentError) + end + + context 'metric is not cached' do + it 'calls fetch_metric' do + expect(subject).to receive(:init_metric).with(metric_type, metric_name, docstring: docstring) + + subject.public_send(metric_name) + end + end + + context 'metric is cached' do + before do + subject.public_send(metric_name) + end + + it 'returns cached metric' do + expect(subject).not_to receive(:init_metric) + + subject.public_send(metric_name) + end + end + end + end + + describe "#fetch_#{metric_type}" do + let(:null_metric) { Gitlab::Metrics::NullMetric.instance } + + define_method(:call_fetch_metric_method) do |**args| + subject.__send__("fetch_#{metric_type}", metric_name, **args) + end + + context "when #{metric_type} is not cached" do + it 'initializes counter metric' do + allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric) + + call_fetch_metric_method(docstring: docstring) + + expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args) + end + end + + context "when #{metric_type} is cached" do + before do + call_fetch_metric_method(docstring: docstring) + end + + it 'uses class metric cache' do + expect(Gitlab::Metrics).not_to receive(metric_type) + + call_fetch_metric_method(docstring: docstring) + end + + context 'when metric is reloaded' do + before do + subject.reload_metric!(metric_name) + end + + it "initializes #{metric_type} metric" do + allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric) + + call_fetch_metric_method(docstring: docstring) + + expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args) + end + end + end + + context 'when metric is configured with feature' do + let(:feature_name) { :some_metric_feature } + let(:metric) { call_fetch_metric_method(docstring: docstring, with_feature: feature_name) } + + context 'when feature is enabled' do + before do + Feature.get(feature_name).enable + end + + it "initializes #{metric_type} metric" do + allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric) + + metric + + expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args) + end + end + + context 'when feature is disabled' do + before do + Feature.get(feature_name).disable + end + + it "returns NullMetric" do + allow(Gitlab::Metrics).to receive(metric_type) + + expect(metric).to be_instance_of(Gitlab::Metrics::NullMetric) + + expect(Gitlab::Metrics).not_to have_received(metric_type) + end + end + end + end + end + + include_examples 'metric', :counter, {} + include_examples 'metric', :gauge, {}, :all + include_examples 'metric', :histogram, {}, [0.005, 0.01, 0.1, 1, 10] +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 375cbf8a9ca..54781dd52fc 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -2,6 +2,11 @@ require 'spec_helper' describe Gitlab::Metrics::Samplers::RubySampler do let(:sampler) { described_class.new(5) } + let(:null_metric) { double('null_metric', set: nil, observe: nil) } + + before do + allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) + end after do Allocations.stop if Gitlab::Metrics.mri? @@ -17,12 +22,9 @@ describe Gitlab::Metrics::Samplers::RubySampler do end it 'adds a metric containing the memory usage' do - expect(Gitlab::Metrics::System).to receive(:memory_usage) - .and_return(9000) + expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000) - expect(sampler.metrics[:memory_usage]).to receive(:set) - .with({}, 9000) - .and_call_original + expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000) sampler.sample end @@ -31,9 +33,7 @@ describe Gitlab::Metrics::Samplers::RubySampler do expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) .and_return(4) - expect(sampler.metrics[:file_descriptors]).to receive(:set) - .with({}, 4) - .and_call_original + expect(sampler.metrics[:file_descriptors]).to receive(:set).with({}, 4) sampler.sample end @@ -49,16 +49,14 @@ describe Gitlab::Metrics::Samplers::RubySampler do it 'adds a metric containing garbage collection time statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(sampler.metrics[:total_time]).to receive(:set) - .with({}, 240) - .and_call_original + expect(sampler.metrics[:total_time]).to receive(:set).with({}, 240) sampler.sample end it 'adds a metric containing garbage collection statistics' do GC.stat.keys.each do |key| - expect(sampler.metrics[key]).to receive(:set).with({}, anything).and_call_original + expect(sampler.metrics[key]).to receive(:set).with({}, anything) end sampler.sample diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index eca75a4fac1..9f3af1acef7 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do end it 'observes view rendering time' do - expect(subscriber.send(:metric_view_rendering_duration_seconds)) + expect(described_class.gitlab_view_rendering_duration_seconds) .to receive(:observe) .with({ view: 'app/views/x.html.haml' }, 2.1) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 9b3698fb4a8..4e7bd433a9c 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do expect(subscriber).to receive(:current_transaction) .at_least(:once) .and_return(transaction) - expect(subscriber.send(:metric_sql_duration_seconds)).to receive(:observe).with({}, 0.002) + expect(described_class.send(:gitlab_sql_duration_seconds)).to receive(:observe).with({}, 0.002) subscriber.sql(event) end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index 58e28592cf9..6795c1ab56b 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -144,7 +144,10 @@ describe Gitlab::Metrics::Subscribers::RailsCache do end context 'with a transaction' do + let(:metric_cache_misses_total) { double('metric_cache_misses_total', increment: nil) } + before do + allow(subscriber).to receive(:metric_cache_misses_total).and_return(metric_cache_misses_total) allow(subscriber).to receive(:current_transaction) .and_return(transaction) end @@ -157,9 +160,9 @@ describe Gitlab::Metrics::Subscribers::RailsCache do end it 'increments the cache_read_miss total' do - expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({}) - subscriber.cache_generate(event) + + expect(metric_cache_misses_total).to have_received(:increment).with({}) end end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 1619fbd88b1..03c185ddc07 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Metrics do context 'prometheus metrics enabled in config' do before do - allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true) + allow(Gitlab::CurrentSettings).to receive(:prometheus_metrics_enabled).and_return(true) end context 'when metrics folder is present' do diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 6334bcd0156..03e0a9e2a03 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -44,6 +44,18 @@ describe Gitlab::OAuth::User do let(:provider) { 'twitter' } + describe 'when account exists on server' do + it 'does not mark the user as external' do + create(:omniauth_user, extern_uid: 'my-uid', provider: provider) + stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider]) + + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + describe 'signup' do context 'when signup is disabled' do before do @@ -51,7 +63,7 @@ describe Gitlab::OAuth::User do end it 'creates the user' do - stub_omniauth_config(allow_single_sign_on: ['twitter']) + stub_omniauth_config(allow_single_sign_on: [provider]) oauth_user.save @@ -65,7 +77,7 @@ describe Gitlab::OAuth::User do end it 'creates and confirms the user anyway' do - stub_omniauth_config(allow_single_sign_on: ['twitter']) + stub_omniauth_config(allow_single_sign_on: [provider]) oauth_user.save @@ -75,7 +87,7 @@ describe Gitlab::OAuth::User do end it 'marks user as having password_automatically_set' do - stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider]) oauth_user.save @@ -86,7 +98,7 @@ describe Gitlab::OAuth::User do shared_examples 'to verify compliance with allow_single_sign_on' do context 'provider is marked as external' do it 'marks user as external' do - stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider]) oauth_user.save expect(gl_user).to be_valid expect(gl_user.external).to be_truthy @@ -95,8 +107,8 @@ describe Gitlab::OAuth::User do context 'provider was external, now has been removed' do it 'does not mark external user as internal' do - create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true) - stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook']) + create(:omniauth_user, extern_uid: 'my-uid', provider: provider, external: true) + stub_omniauth_config(allow_single_sign_on: [provider], external_providers: ['facebook']) oauth_user.save expect(gl_user).to be_valid expect(gl_user.external).to be_truthy @@ -118,7 +130,7 @@ describe Gitlab::OAuth::User do context 'with new allow_single_sign_on enabled syntax' do before do - stub_omniauth_config(allow_single_sign_on: ['twitter']) + stub_omniauth_config(allow_single_sign_on: [provider]) end it "creates a user from Omniauth" do @@ -127,7 +139,7 @@ describe Gitlab::OAuth::User do expect(gl_user).to be_valid identity = gl_user.identities.first expect(identity.extern_uid).to eql uid - expect(identity.provider).to eql 'twitter' + expect(identity.provider).to eql provider end end @@ -142,7 +154,7 @@ describe Gitlab::OAuth::User do expect(gl_user).to be_valid identity = gl_user.identities.first expect(identity.extern_uid).to eql uid - expect(identity.provider).to eql 'twitter' + expect(identity.provider).to eql provider end end @@ -275,6 +287,26 @@ describe Gitlab::OAuth::User do end end + context 'and a corresponding LDAP person with a non-default username' do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { 'johndoe@example.com' } + allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) } + allow(ldap_user).to receive(:dn) { dn } + end + + context 'and no account for the LDAP user' do + it 'creates a user favoring the LDAP username and strips email domain' do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'johndoe' + end + end + end + context "and no corresponding LDAP person" do before do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 0ae90069b7f..85991c38363 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -121,7 +121,7 @@ describe Gitlab::PathRegex do STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} - WILDCARD_SEGMENT = %r{\*} + WILDCARD_SEGMENT = /\*/ let(:namespaced_wildcard_routes) do routes_without_format.select do |p| p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb new file mode 100644 index 00000000000..2e2cb4ca28f --- /dev/null +++ b/spec/lib/gitlab/popen/runner_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe Gitlab::Popen::Runner do + subject { described_class.new } + + describe '#run' do + it 'runs the command and returns the result' do + run_command + + expect(Gitlab::Popen).to have_received(:popen_with_detail) + end + end + + describe '#all_success_and_clean?' do + it 'returns true when exit status is 0 and stderr is empty' do + run_command + + expect(subject).to be_all_success_and_clean + end + + it 'returns false when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).not_to be_all_success_and_clean + end + + it 'returns false when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject).not_to be_all_success_and_clean + end + end + + describe '#all_success?' do + it 'returns true when exit status is 0' do + run_command + + expect(subject).to be_all_success + end + + it 'returns false when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).not_to be_all_success + end + + it 'returns true' do + run_command(stderr: 'stderr') + + expect(subject).to be_all_success + end + end + + describe '#all_stderr_empty?' do + it 'returns true when stderr is empty' do + run_command + + expect(subject).to be_all_stderr_empty + end + + it 'returns true when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).to be_all_stderr_empty + end + + it 'returns false when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject).not_to be_all_stderr_empty + end + end + + describe '#failed_results' do + it 'returns [] when everything is passed' do + run_command + + expect(subject.failed_results).to be_empty + end + + it 'returns the result when exit status is not 0' do + result = run_command(exitstatus: 1) + + expect(subject.failed_results).to contain_exactly(result) + end + + it 'returns [] when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject.failed_results).to be_empty + end + end + + describe '#warned_results' do + it 'returns [] when everything is passed' do + run_command + + expect(subject.warned_results).to be_empty + end + + it 'returns [] when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject.warned_results).to be_empty + end + + it 'returns the result when exit stderr has something' do + result = run_command(stderr: 'stderr') + + expect(subject.warned_results).to contain_exactly(result) + end + end + + def run_command( + command: 'command', + stdout: 'stdout', + stderr: '', + exitstatus: 0, + status: double(exitstatus: exitstatus, success?: exitstatus.zero?), + duration: 0.1) + + result = + Gitlab::Popen::Result.new(command, stdout, stderr, status, duration) + + allow(Gitlab::Popen) + .to receive(:popen_with_detail) + .and_return(result) + + subject.run([command]) do |cmd, &run| + expect(cmd).to eq(command) + + cmd_result = run.call + + expect(cmd_result).to eq(result) + end + + subject.results.first + end +end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index b145ca36f26..1dbead16d5b 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -1,11 +1,23 @@ require 'spec_helper' -describe 'Gitlab::Popen' do +describe Gitlab::Popen do let(:path) { Rails.root.join('tmp').to_s } before do @klass = Class.new(Object) - @klass.send(:include, Gitlab::Popen) + @klass.send(:include, described_class) + end + + describe '.popen_with_detail' do + subject { @klass.new.popen_with_detail(cmd) } + + let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1);$stderr.puts(2);exit(3)] } + + it { expect(subject.cmd).to eq(cmd) } + it { expect(subject.stdout).to eq("1\n") } + it { expect(subject.stderr).to eq("2\n") } + it { expect(subject.status.exitstatus).to eq(3) } + it { expect(subject.duration).to be_kind_of(Numeric) } end context 'zero status' do diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb new file mode 100644 index 00000000000..4a43dbb2371 --- /dev/null +++ b/spec/lib/gitlab/profiler_spec.rb @@ -0,0 +1,156 @@ +require 'spec_helper' + +describe Gitlab::Profiler do + RSpec::Matchers.define_negated_matcher :not_change, :change + + let(:null_logger) { Logger.new('/dev/null') } + let(:private_token) { 'private' } + + describe '.profile' do + let(:app) { double(:app) } + + before do + allow(ActionDispatch::Integration::Session).to receive(:new).and_return(app) + allow(app).to receive(:get) + end + + it 'returns a profile result' do + expect(described_class.profile('/')).to be_an_instance_of(RubyProf::Profile) + end + + it 'uses the custom logger given' do + expect(described_class).to receive(:create_custom_logger) + .with(null_logger, private_token: anything) + .and_call_original + + described_class.profile('/', logger: null_logger) + end + + it 'sends a POST request when data is passed' do + post_data = '{"a":1}' + + expect(app).to receive(:post).with(anything, post_data, anything) + + described_class.profile('/', post_data: post_data) + end + + it 'uses the private_token for auth if given' do + expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token) + expect(app).to receive(:get).with('/api/v4/users') + + described_class.profile('/', private_token: private_token) + end + + it 'uses the user for auth if given' do + user = double(:user) + user_token = 'user' + + allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token) + + expect(app).to receive(:get).with('/', nil, 'Private-Token' => user_token) + expect(app).to receive(:get).with('/api/v4/users') + + described_class.profile('/', user: user) + end + + it 'uses the private_token for auth if both it and user are set' do + user = double(:user) + user_token = 'user' + + allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token) + + expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token) + expect(app).to receive(:get).with('/api/v4/users') + + described_class.profile('/', user: user, private_token: private_token) + end + end + + describe '.create_custom_logger' do + it 'does nothing when nil is passed' do + expect(described_class.create_custom_logger(nil)).to be_nil + end + + context 'the new logger' do + let(:custom_logger) do + described_class.create_custom_logger(null_logger, private_token: private_token) + end + + it 'does not affect the existing logger' do + expect(null_logger).not_to receive(:debug) + expect(custom_logger).to receive(:debug).and_call_original + + custom_logger.debug('Foo') + end + + it 'strips out the private token' do + expect(custom_logger).to receive(:add) do |severity, _progname, message| + expect(severity).to eq(Logger::DEBUG) + expect(message).to include('public').and include(described_class::FILTERED_STRING) + expect(message).not_to include(private_token) + end + + custom_logger.debug("public #{private_token}") + end + + it 'tracks model load times by model' do + custom_logger.debug('This is not a model load') + custom_logger.debug('User Load (1.2ms)') + custom_logger.debug('User Load (1.3ms)') + custom_logger.debug('Project Load (10.4ms)') + + expect(custom_logger.load_times_by_model).to eq('User' => 2.5, + 'Project' => 10.4) + end + + it 'logs the backtrace, ignoring lines as appropriate' do + # Skip Rails's backtrace cleaning. + allow(Rails.backtrace_cleaner).to receive(:clean, &:itself) + + expect(custom_logger).to receive(:add) + .with(Logger::DEBUG, + anything, + a_string_matching(File.basename(__FILE__))) + .twice + + expect(custom_logger).not_to receive(:add).with(Logger::DEBUG, + anything, + a_string_matching('lib/gitlab/profiler.rb')) + + # Force a part of the backtrace to be in the (ignored) profiler source + # file. + described_class.with_custom_logger(nil) { custom_logger.debug('Foo') } + end + end + end + + describe '.with_custom_logger' do + context 'when the logger is set' do + it 'uses the replacement logger for the duration of the block' do + expect(null_logger).to receive(:debug).and_call_original + + expect { described_class.with_custom_logger(null_logger) { ActiveRecord::Base.logger.debug('foo') } } + .to not_change { ActiveRecord::Base.logger } + .and not_change { ActionController::Base.logger } + .and not_change { ActiveSupport::LogSubscriber.colorize_logging } + end + + it 'returns the result of the block' do + expect(described_class.with_custom_logger(null_logger) { 2 }).to eq(2) + end + end + + context 'when the logger is nil' do + it 'returns the result of the block' do + expect(described_class.with_custom_logger(nil) { 2 }).to eq(2) + end + + it 'does not modify the standard Rails loggers' do + expect { described_class.with_custom_logger(nil) { } } + .to not_change { ActiveRecord::Base.logger } + .and not_change { ActionController::Base.logger } + .and not_change { ActiveSupport::LogSubscriber.colorize_logging } + end + end + end +end diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index 953cfbb8b88..f3cd6961e94 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::ProjectAuthorizations do end before do - other_project.team << [user, :reporter] + other_project.add_reporter(user) group.add_developer(user) end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index a424f0f5cfe..1ebb0105cf5 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -70,15 +70,6 @@ describe Gitlab::ProjectSearchResults do subject { described_class.parse_search_result(search_result) } - it 'can correctly parse filenames including ":"' do - special_char_result = "\nmaster:testdata/project::function1.yaml-1----\nmaster:testdata/project::function1.yaml:2:test: data1\n" - - blob = described_class.parse_search_result(special_char_result) - - expect(blob.ref).to eq('master') - expect(blob.filename).to eq('testdata/project::function1.yaml') - end - it "returns a valid FoundBlob" do is_expected.to be_an Gitlab::SearchResults::FoundBlob expect(subject.id).to be_nil @@ -90,8 +81,32 @@ describe Gitlab::ProjectSearchResults do expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") end + context 'when the matching filename contains a colon' do + let(:search_result) { "\nmaster:testdata/project::function1.yaml\x001\x00---\n" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/project::function1.yaml') + expect(subject.basename).to eq('testdata/project::function1') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq('---') + end + end + + context 'when the matching content contains a number surrounded by colons' do + let(:search_result) { "\nmaster:testdata/foo.txt\x001\x00blah:9:blah" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/foo.txt') + expect(subject.basename).to eq('testdata/foo') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq('blah:9:blah') + end + end + context "when filename has extension" do - let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" } it { expect(subject.path).to eq('CONTRIBUTE.md') } it { expect(subject.filename).to eq('CONTRIBUTE.md') } @@ -99,7 +114,7 @@ describe Gitlab::ProjectSearchResults do end context "when file under directory" do - let(:search_result) { "master:a/b/c.md:5:a b c\n" } + let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" } it { expect(subject.path).to eq('a/b/c.md') } it { expect(subject.filename).to eq('a/b/c.md') } @@ -144,7 +159,7 @@ describe Gitlab::ProjectSearchResults do end it 'finds by content' do - expect(results).to include("master:Title.md:1:Content\n") + expect(results).to include("master:Title.md\x001\x00Content\n") end end @@ -179,7 +194,7 @@ describe Gitlab::ProjectSearchResults do end it 'does not list project confidential issues for project members with guest role' do - project.team << [member, :guest] + project.add_guest(member) results = described_class.new(member, project, query) issues = results.objects('issues') @@ -211,7 +226,7 @@ describe Gitlab::ProjectSearchResults do end it 'lists project confidential issues for project members' do - project.team << [member, :developer] + project.add_developer(member) results = described_class.new(member, project, query) issues = results.objects('issues') @@ -290,12 +305,12 @@ describe Gitlab::ProjectSearchResults do let!(:private_project) { create(:project, :private, :repository, creator: creator, namespace: creator.namespace) } let(:team_master) do user = create(:user, username: 'private-project-master') - private_project.team << [user, :master] + private_project.add_master(user) user end let(:team_reporter) do user = create(:user, username: 'private-project-reporter') - private_project.team << [user, :reporter] + private_project.add_reporter(user) user end diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb new file mode 100644 index 00000000000..b49bc5c328c --- /dev/null +++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::ActiveSupportSubscriber do + describe '#sql' do + it 'increments the number of executed SQL queries' do + transaction = double(:transaction) + + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + + expect(transaction) + .to receive(:increment) + .at_least(:once) + + User.count + end + end +end diff --git a/spec/lib/gitlab/query_limiting/middleware_spec.rb b/spec/lib/gitlab/query_limiting/middleware_spec.rb new file mode 100644 index 00000000000..a04bcdecb4b --- /dev/null +++ b/spec/lib/gitlab/query_limiting/middleware_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::Middleware do + describe '#call' do + it 'runs the application with query limiting in place' do + middleware = described_class.new(-> (env) { env }) + + expect_any_instance_of(Gitlab::QueryLimiting::Transaction) + .to receive(:act_upon_results) + + expect(middleware.call({ number: 10 })) + .to eq({ number: 10 }) + end + end + + describe '#action_name' do + let(:middleware) { described_class.new(-> (env) { env }) } + + context 'using a Rails request' do + it 'returns the name of the controller and action' do + env = { + described_class::CONTROLLER_KEY => double( + :controller, + action_name: 'show', + class: double(:class, name: 'UsersController'), + content_type: 'text/html' + ) + } + + expect(middleware.action_name(env)).to eq('UsersController#show') + end + + it 'includes the content type if this is not text/html' do + env = { + described_class::CONTROLLER_KEY => double( + :controller, + action_name: 'show', + class: double(:class, name: 'UsersController'), + content_type: 'application/json' + ) + } + + expect(middleware.action_name(env)) + .to eq('UsersController#show (application/json)') + end + end + + context 'using a Grape API request' do + it 'returns the name of the request method and endpoint path' do + env = { + described_class::ENDPOINT_KEY => double( + :endpoint, + route: double(:route, request_method: 'GET', path: '/foo') + ) + } + + expect(middleware.action_name(env)).to eq('GET /foo') + end + + it 'returns nil if the route can not be retrieved' do + endpoint = double(:endpoint) + env = { described_class::ENDPOINT_KEY => endpoint } + + allow(endpoint) + .to receive(:route) + .and_raise(RuntimeError) + + expect(middleware.action_name(env)).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb new file mode 100644 index 00000000000..b4231fcd0fa --- /dev/null +++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::Transaction do + after do + Thread.current[described_class::THREAD_KEY] = nil + end + + describe '.current' do + it 'returns nil when there is no transaction' do + expect(described_class.current).to be_nil + end + + it 'returns the transaction when present' do + Thread.current[described_class::THREAD_KEY] = described_class.new + + expect(described_class.current).to be_an_instance_of(described_class) + end + end + + describe '.run' do + it 'runs a transaction and returns it and its return value' do + trans, ret = described_class.run do + 10 + end + + expect(trans).to be_an_instance_of(described_class) + expect(ret).to eq(10) + end + + it 'removes the transaction from the current thread upon completion' do + described_class.run do + 10 + end + + expect(Thread.current[described_class::THREAD_KEY]).to be_nil + end + end + + describe '#act_upon_results' do + context 'when the query threshold is not exceeded' do + it 'does nothing' do + trans = described_class.new + + expect(trans).not_to receive(:raise) + + trans.act_upon_results + end + end + + context 'when the query threshold is exceeded' do + let(:transaction) do + trans = described_class.new + trans.count = described_class::THRESHOLD + 1 + + trans + end + + it 'raises an error when this is enabled' do + expect { transaction.act_upon_results } + .to raise_error(described_class::ThresholdExceededError) + end + + it 'reports the error in Sentry if raising an error is disabled' do + expect(transaction) + .to receive(:raise_error?) + .and_return(false) + + expect(Raven) + .to receive(:capture_exception) + .with(an_instance_of(described_class::ThresholdExceededError)) + + transaction.act_upon_results + end + end + end + + describe '#increment' do + it 'increments the number of executed queries' do + transaction = described_class.new + + expect(transaction.count).to be_zero + + transaction.increment + + expect(transaction.count).to eq(1) + end + end + + describe '#raise_error?' do + it 'returns true in a test environment' do + transaction = described_class.new + + expect(transaction.raise_error?).to eq(true) + end + + it 'returns false in a production environment' do + transaction = described_class.new + + expect(Rails.env) + .to receive(:test?) + .and_return(false) + + expect(transaction.raise_error?).to eq(false) + end + end + + describe '#threshold_exceeded?' do + it 'returns false when the threshold is not exceeded' do + transaction = described_class.new + + expect(transaction.threshold_exceeded?).to eq(false) + end + + it 'returns true when the threshold is exceeded' do + transaction = described_class.new + transaction.count = described_class::THRESHOLD + 1 + + expect(transaction.threshold_exceeded?).to eq(true) + end + end + + describe '#error_message' do + it 'returns the error message to display when the threshold is exceeded' do + transaction = described_class.new + transaction.count = max = described_class::THRESHOLD + + expect(transaction.error_message).to eq( + "Too many SQL queries were executed: a maximum of #{max} " \ + "is allowed but #{max} SQL queries were executed" + ) + end + + it 'includes the action name in the error message when present' do + transaction = described_class.new + transaction.count = max = described_class::THRESHOLD + transaction.action = 'UsersController#show' + + expect(transaction.error_message).to eq( + "Too many SQL queries were executed in UsersController#show: " \ + "a maximum of #{max} is allowed but #{max} SQL queries were executed" + ) + end + end +end diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb new file mode 100644 index 00000000000..2eddab0b8c3 --- /dev/null +++ b/spec/lib/gitlab/query_limiting_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting do + describe '.enable?' do + it 'returns true in a test environment' do + expect(described_class.enable?).to eq(true) + end + + it 'returns true in a development environment' do + allow(Rails.env).to receive(:development?).and_return(true) + + expect(described_class.enable?).to eq(true) + end + + it 'returns true on GitLab.com' do + allow(Gitlab).to receive(:com?).and_return(true) + + expect(described_class.enable?).to eq(true) + end + + it 'returns true in a non GitLab.com' do + expect(Gitlab).to receive(:com?).and_return(false) + expect(Rails.env).to receive(:development?).and_return(false) + expect(Rails.env).to receive(:test?).and_return(false) + + expect(described_class.enable?).to eq(false) + end + end + + describe '.whitelist' do + it 'raises ArgumentError when an invalid issue URL is given' do + expect { described_class.whitelist('foo') } + .to raise_error(ArgumentError) + end + + context 'without a transaction' do + it 'does nothing' do + expect { described_class.whitelist('https://example.com') } + .not_to raise_error + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::QueryLimiting::Transaction.new } + + before do + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + end + + it 'does not increment the number of SQL queries executed in the block' do + before = transaction.count + + described_class.whitelist('https://example.com') + + 2.times do + User.count + end + + expect(transaction.count).to eq(before) + end + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 8ec3f55e6de..4139d1c650c 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::ReferenceExtractor do let(:project) { create(:project) } before do - project.team << [project.creator, :developer] + project.add_developer(project.creator) end subject { described_class.new(project, project.creator) } @@ -14,8 +14,8 @@ describe Gitlab::ReferenceExtractor do @u_bar = create(:user, username: 'bar') @u_offteam = create(:user, username: 'offteam') - project.team << [@u_foo, :reporter] - project.team << [@u_bar, :guest] + project.add_guest(@u_foo) + project.add_guest(@u_bar) subject.analyze('@foo, @baduser, @bar, and @offteam') expect(subject.users).to match_array([@u_foo, @u_bar, @u_offteam]) @@ -26,8 +26,8 @@ describe Gitlab::ReferenceExtractor do @u_bar = create(:user, username: 'bar') @u_offteam = create(:user, username: 'offteam') - project.team << [@u_foo, :reporter] - project.team << [@u_bar, :guest] + project.add_reporter(@u_foo) + project.add_reporter(@u_bar) subject.analyze(%Q{ Inline code: `@foo` @@ -228,7 +228,7 @@ describe Gitlab::ReferenceExtractor do let(:issue) { create(:issue, project: other_project) } before do - other_project.team << [project.creator, :developer] + other_project.add_developer(project.creator) end it 'handles project issue references' do @@ -246,7 +246,7 @@ describe Gitlab::ReferenceExtractor do let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" } before do - project.team << [project.creator, :developer] + project.add_developer(project.creator) subject.analyze(text) end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 68a57826647..8b54d72d6f7 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Regex do it { is_expected.not_to match('?gitlab') } end - describe '.environment_slug_regex' do + describe '.environment_name_regex' do subject { described_class.environment_name_regex } it { is_expected.to match('foo') } @@ -24,6 +24,7 @@ describe Gitlab::Regex do it { is_expected.to match('foo.1') } it { is_expected.not_to match('9&foo') } it { is_expected.not_to match('foo-^') } + it { is_expected.not_to match('!!()()') } end describe '.environment_slug_regex' do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index a958baab44f..17b48b3d062 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -16,7 +16,13 @@ describe Gitlab::SearchResults do context 'as a user with access' do before do - project.team << [user, :developer] + project.add_developer(user) + end + + describe '#objects' do + it 'returns without_page collection by default' do + expect(results.objects('projects')).to be_kind_of(Kaminari::PaginatableWithoutCount) + end end describe '#projects_count' do @@ -43,6 +49,58 @@ describe Gitlab::SearchResults do end end + context "when count_limit is lower than total amount" do + before do + allow(results).to receive(:count_limit).and_return(1) + end + + describe '#limited_projects_count' do + it 'returns the limited amount of projects' do + create(:project, name: 'foo2') + + expect(results.limited_projects_count).to eq(1) + end + end + + describe '#limited_merge_requests_count' do + it 'returns the limited amount of merge requests' do + create(:merge_request, :simple, source_project: project, title: 'foo2') + + expect(results.limited_merge_requests_count).to eq(1) + end + end + + describe '#limited_milestones_count' do + it 'returns the limited amount of milestones' do + create(:milestone, project: project, title: 'foo2') + + expect(results.limited_milestones_count).to eq(1) + end + end + + describe '#limited_issues_count' do + it 'runs single SQL query to get the limited amount of issues' do + create(:milestone, project: project, title: 'foo2') + + expect(results).to receive(:issues).with(public_only: true).and_call_original + expect(results).not_to receive(:issues).with(no_args).and_call_original + + expect(results.limited_issues_count).to eq(1) + end + end + end + + context "when count_limit is higher than total amount" do + describe '#limited_issues_count' do + it 'runs multiple queries to get the limited amount of issues' do + expect(results).to receive(:issues).with(public_only: true).and_call_original + expect(results).to receive(:issues).with(no_args).and_call_original + + expect(results.limited_issues_count).to eq(1) + end + end + end + it 'includes merge requests from source and target projects' do forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') @@ -52,15 +110,36 @@ describe Gitlab::SearchResults do expect(results.objects('merge_requests')).to include merge_request_2 end - it 'includes project filter by default' do - expect(results).to receive(:project_ids_relation).and_call_original - results.objects('merge_requests') + describe '#merge_requests' do + it 'includes project filter by default' do + expect(results).to receive(:project_ids_relation).and_call_original + + results.objects('merge_requests') + end + + it 'it skips project filter if default project context is used' do + allow(results).to receive(:default_project_filter).and_return(true) + + expect(results).not_to receive(:project_ids_relation) + + results.objects('merge_requests') + end end - it 'it skips project filter if default is used' do - allow(results).to receive(:default_project_filter).and_return(true) - expect(results).not_to receive(:project_ids_relation) - results.objects('merge_requests') + describe '#issues' do + it 'includes project filter by default' do + expect(results).to receive(:project_ids_relation).and_call_original + + results.objects('issues') + end + + it 'it skips project filter if default project context is used' do + allow(results).to receive(:default_project_filter).and_return(true) + + expect(results).not_to receive(:project_ids_relation) + + results.objects('issues') + end end end @@ -104,8 +183,8 @@ describe Gitlab::SearchResults do end it 'does not list confidential issues for project members with guest role' do - project_1.team << [member, :guest] - project_2.team << [member, :guest] + project_1.add_guest(member) + project_2.add_guest(member) results = described_class.new(member, limit_projects, query) issues = results.objects('issues') @@ -146,8 +225,8 @@ describe Gitlab::SearchResults do end it 'lists confidential issues for project members' do - project_1.team << [member, :developer] - project_2.team << [member, :developer] + project_1.add_developer(member) + project_2.add_developer(member) results = described_class.new(member, limit_projects, query) issues = results.objects('issues') diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index dd779b04741..2b61ce38418 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -4,6 +4,7 @@ require 'stringio' describe Gitlab::Shell do set(:project) { create(:project, :repository) } + let(:repository) { project.repository } let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } let(:gitlab_projects) { double('gitlab_projects') } @@ -51,6 +52,311 @@ describe Gitlab::Shell do end end + describe '#add_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + end + + describe '#batch_add_keys' do + context 'when authorized_keys_enabled is true' do + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + end + + describe '#remove_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when key content is not given' do + it 'calls rm-key with only one argument' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123'] + ) + + gitlab_shell.remove_key('key-123') + end + end + end + + describe '#remove_all_keys' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'clear'] + ) + + gitlab_shell.remove_all_keys + end + end + end + + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + expect(find_in_authorized_keys_file(9876)).to be_truthy + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + expect(find_in_authorized_keys_file(9876)).to be_falsey + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + end + end + + context 'when keys there are duplicate keys in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + + it 'does not run remove more than once per key (in a batch)' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234').once + gitlab_shell.remove_keys_not_found_in_db + end + end + + context 'when keys there are duplicate keys in the file that ARE in the DB' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end + + it 'does not remove the key' do + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(@key.id)).to be_truthy + end + + it 'does not need to run a SELECT query for that batch, on account of that key' do + expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) + gitlab_shell.remove_keys_not_found_in_db + end + end + + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + end + end + end + + describe '#batch_read_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'iterates over the key IDs in the file, in batches' do + loop_count = 0 + first_batch = [1, 2] + second_batch = [3, 4] + + gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| + expected = (loop_count == 0 ? first_batch : second_batch) + expect(batch).to eq(expected) + loop_count += 1 + end + end + end + end + + describe '#list_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'outputs the key IDs in the file, separated by newlines' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) + end + end + + context 'when there are no keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + end + + it 'outputs nothing, not even an empty string' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq([]) + end + end + end + describe Gitlab::Shell::KeyAdder do describe '#add_key' do it 'removes trailing garbage' do @@ -96,17 +402,6 @@ describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - describe '#add_repository' do shared_examples '#add_repository' do let(:repository_storage) { 'default' } @@ -148,32 +443,44 @@ describe Gitlab::Shell do end describe '#remove_repository' do - subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) } + let!(:project) { create(:project, :repository) } + let(:disk_path) { "#{project.disk_path}.git" } it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:rm_project) { true } + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(true) - is_expected.to be_truthy + expect(gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)).to be(true) + + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) end - it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:rm_project) { false } + it 'keeps the namespace directory' do + gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) - is_expected.to be_falsy + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) + expect(gitlab_shell.exists?(project.repository_storage_path, project.disk_path.gsub(project.name, ''))).to be(true) end end describe '#mv_repository' do + let!(:project2) { create(:project, :repository) } + it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true } + old_path = project2.disk_path + new_path = "project/new_path" - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(true) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(false) + + expect(gitlab_shell.mv_repository(project2.repository_storage_path, old_path, new_path)).to be_truthy + + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(false) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(true) end it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false } - - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy + expect(gitlab_shell.mv_repository(project2.repository_storage_path, project2.disk_path, '')).to be_falsy + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{project2.disk_path}.git")).to be(true) end end @@ -201,8 +508,6 @@ describe Gitlab::Shell do end shared_examples 'fetch_remote' do |gitaly_on| - let(:repository) { project.repository } - def fetch_remote(ssh_auth = nil) gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth) end @@ -325,6 +630,23 @@ describe Gitlab::Shell do describe '#fetch_remote gitaly' do it_should_behave_like 'fetch_remote', true + + context 'gitaly call' do + let(:remote_name) { 'remote-name' } + let(:ssh_auth) { double(:ssh_auth) } + + subject do + gitlab_shell.fetch_remote(repository.raw_repository, remote_name, + forced: true, no_tags: true, ssh_auth: ssh_auth) + end + + it 'passes the correct params to the gitaly service' do + expect(repository.gitaly_repository_client).to receive(:fetch_remote) + .with(remote_name, ssh_auth: ssh_auth, forced: true, no_tags: true, timeout: timeout) + + subject + end + end end describe '#import_repository' do @@ -347,62 +669,6 @@ describe Gitlab::Shell do end.to raise_error(Gitlab::Shell::Error, "error") end end - - describe '#push_remote_branches' do - subject(:result) do - gitlab_shell.push_remote_branches( - project.repository_storage_path, - project.disk_path, - 'downstream-remote', - ['master'] - ) - end - - it 'executes the command' do - expect(gitlab_projects).to receive(:push_branches) - .with('downstream-remote', timeout, true, ['master']) - .and_return(true) - - is_expected.to be_truthy - end - - it 'fails to execute the command' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:push_branches) - .with('downstream-remote', timeout, true, ['master']) - .and_return(false) - - expect { result }.to raise_error(Gitlab::Shell::Error, 'error') - end - end - - describe '#delete_remote_branches' do - subject(:result) do - gitlab_shell.delete_remote_branches( - project.repository_storage_path, - project.disk_path, - 'downstream-remote', - ['master'] - ) - end - - it 'executes the command' do - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(true) - - is_expected.to be_truthy - end - - it 'fails to execute the command' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(false) - - expect { result }.to raise_error(Gitlab::Shell::Error, 'error') - end - end end describe 'namespace actions' do @@ -452,4 +718,12 @@ describe Gitlab::Shell do end end end + + def find_in_authorized_keys_file(key_id) + gitlab_shell.batch_read_key_ids do |ids| + return true if ids.include?(key_id) + end + + false + end end diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 75ae58d0582..3b077c58c50 100644 --- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::SlashCommands::IssueNew do let(:regex_match) { described_class.match("issue create bird is the word") } before do - project.team << [user, :master] + project.add_master(user) end subject do diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index 51f59216413..35d01efc1bd 100644 --- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } let(:project) { create(:project) } - let(:user) { issue.author } + let(:user) { create(:user) } let(:regex_match) { described_class.match("issue search find") } subject do @@ -21,7 +21,7 @@ describe Gitlab::SlashCommands::IssueSearch do context 'the user has access' do before do - project.team << [user, :master] + project.add_master(user) end it 'returns all results' do diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb index 08c380ca8f1..e5834d5a2ee 100644 --- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::SlashCommands::IssueShow do let(:regex_match) { described_class.match("issue show #{issue.iid}") } before do - project.team << [user, :master] + project.add_master(user) end subject do diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index 93d538141ce..c15e29774b6 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do end end + describe '.sanitize(key_content)' do + let(:content) { build(:key).key } + + context 'when key has blank space characters' do + it 'removes the extra blank space characters' do + unsanitized = content.insert(100, "\n") + .insert(40, "\r\n") + .insert(30, ' ') + + sanitized = described_class.sanitize(unsanitized) + _, body = sanitized.split + + expect(sanitized).not_to eq(unsanitized) + expect(body).not_to match(/\s/) + end + end + + context "when key doesn't have blank space characters" do + it "doesn't modify the content" do + sanitized = described_class.sanitize(content) + + expect(sanitized).to eq(content) + end + end + + context "when key is invalid" do + it 'returns the original content' do + unsanitized = "ssh-foo any content==" + sanitized = described_class.sanitize(unsanitized) + + expect(sanitized).to eq(unsanitized) + end + end + end + describe '#valid?' do subject { public_key } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b5f2a15ada3..0e9ecff25a6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -103,9 +103,9 @@ describe Gitlab::UsageData do subject { described_class.features_usage_data_ce } it 'gathers feature usage data' do - expect(subject[:signup]).to eq(current_application_settings.allow_signup?) + expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?) expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled) - expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?) + expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?) expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled) expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?) expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled) @@ -129,7 +129,7 @@ describe Gitlab::UsageData do subject { described_class.license_usage_data } it "gathers license data" do - expect(subject[:uuid]).to eq(current_application_settings.uuid) + expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index cd97416bcc9..7280acb6c82 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -8,19 +8,19 @@ describe Gitlab::UserAccess do describe '#can_push_to_branch?' do describe 'push to none protected branch' do it 'returns true if user is a master' do - project.team << [user, :master] + project.add_master(user) expect(access.can_push_to_branch?('random_branch')).to be_truthy end it 'returns true if user is a developer' do - project.team << [user, :developer] + project.add_developer(user) expect(access.can_push_to_branch?('random_branch')).to be_truthy end it 'returns false if user is a reporter' do - project.team << [user, :reporter] + project.add_reporter(user) expect(access.can_push_to_branch?('random_branch')).to be_falsey end @@ -31,34 +31,34 @@ describe Gitlab::UserAccess do let(:project_access) { described_class.new(user, project: empty_project) } it 'returns true if user is master' do - empty_project.team << [user, :master] + empty_project.add_master(user) expect(project_access.can_push_to_branch?('master')).to be_truthy end it 'returns false if user is developer and project is fully protected' do - empty_project.team << [user, :developer] + empty_project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) expect(project_access.can_push_to_branch?('master')).to be_falsey end it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do - empty_project.team << [user, :developer] + empty_project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) expect(project_access.can_push_to_branch?('master')).to be_falsey end it 'returns true if user is developer and project is unprotected' do - empty_project.team << [user, :developer] + empty_project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) expect(project_access.can_push_to_branch?('master')).to be_truthy end it 'returns true if user is developer and project grants developers permission' do - empty_project.team << [user, :developer] + empty_project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) expect(project_access.can_push_to_branch?('master')).to be_truthy @@ -70,25 +70,25 @@ describe Gitlab::UserAccess do let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } it 'returns true if user is a master' do - project.team << [user, :master] + project.add_master(user) expect(access.can_push_to_branch?(branch.name)).to be_truthy end it 'returns false if user is a developer' do - project.team << [user, :developer] + project.add_developer(user) expect(access.can_push_to_branch?(branch.name)).to be_falsey end it 'returns false if user is a reporter' do - project.team << [user, :reporter] + project.add_reporter(user) expect(access.can_push_to_branch?(branch.name)).to be_falsey end it 'returns false if branch does not exist' do - project.team << [user, :developer] + project.add_developer(user) expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey end @@ -100,19 +100,19 @@ describe Gitlab::UserAccess do end it 'returns true if user is a master' do - project.team << [user, :master] + project.add_master(user) expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it 'returns true if user is a developer' do - project.team << [user, :developer] + project.add_developer(user) expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it 'returns false if user is a reporter' do - project.team << [user, :reporter] + project.add_reporter(user) expect(access.can_push_to_branch?(@branch.name)).to be_falsey end @@ -124,19 +124,19 @@ describe Gitlab::UserAccess do end it 'returns true if user is a master' do - project.team << [user, :master] + project.add_master(user) expect(access.can_merge_to_branch?(@branch.name)).to be_truthy end it 'returns true if user is a developer' do - project.team << [user, :developer] + project.add_developer(user) expect(access.can_merge_to_branch?(@branch.name)).to be_truthy end it 'returns false if user is a reporter' do - project.team << [user, :reporter] + project.add_reporter(user) expect(access.can_merge_to_branch?(@branch.name)).to be_falsey end diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb new file mode 100644 index 00000000000..7c97cee982a --- /dev/null +++ b/spec/lib/gitlab/utils/override_spec.rb @@ -0,0 +1,158 @@ +require 'spec_helper' + +describe Gitlab::Utils::Override do + let(:base) { Struct.new(:good) } + + let(:derived) { Class.new(base).tap { |m| m.extend described_class } } + let(:extension) { Module.new.tap { |m| m.extend described_class } } + + let(:prepending_class) { base.tap { |m| m.prepend extension } } + let(:including_class) { base.tap { |m| m.include extension } } + + let(:klass) { subject } + + def good(mod) + mod.module_eval do + override :good + def good + super.succ + end + end + + mod + end + + def bad(mod) + mod.module_eval do + override :bad + def bad + true + end + end + + mod + end + + shared_examples 'checking as intended' do + it 'checks ok for overriding method' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(1) + described_class.verify! + end + + it 'raises NotImplementedError when it is not overriding anything' do + expect do + bad(subject) + klass.new(0).bad + described_class.verify! + end.to raise_error(NotImplementedError) + end + end + + shared_examples 'nothing happened' do + it 'does not complain when it is overriding something' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(1) + described_class.verify! + end + + it 'does not complain when it is not overriding anything' do + bad(subject) + result = klass.new(0).bad + + expect(result).to eq(true) + described_class.verify! + end + end + + before do + # Make sure we're not touching the internal cache + allow(described_class).to receive(:extensions).and_return({}) + end + + describe '#override' do + context 'when STATIC_VERIFICATION is set' do + before do + stub_env('STATIC_VERIFICATION', 'true') + end + + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'checking as intended' + end + + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'checking as intended' + end + + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } + + it 'raises NotImplementedError because it is not overriding it' do + expect do + good(subject) + klass.new(0).good + described_class.verify! + end.to raise_error(NotImplementedError) + end + + it 'raises NotImplementedError when it is not overriding anything' do + expect do + bad(subject) + klass.new(0).bad + described_class.verify! + end.to raise_error(NotImplementedError) + end + end + end + end + + context 'when STATIC_VERIFICATION is not set' do + before do + stub_env('STATIC_VERIFICATION', nil) + end + + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'nothing happened' + end + + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'nothing happened' + end + + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } + + it 'does not complain when it is overriding something' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(0) + described_class.verify! + end + + it 'does not complain when it is not overriding anything' do + bad(subject) + result = klass.new(0).bad + + expect(result).to eq(true) + described_class.verify! + end + end + end +end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index e872a5290c5..bda239b7871 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -17,6 +17,22 @@ describe Gitlab::Utils do end end + describe '.remove_line_breaks' do + using RSpec::Parameterized::TableSyntax + + where(:original, :expected) do + "foo\nbar\nbaz" | "foobarbaz" + "foo\r\nbar\r\nbaz" | "foobarbaz" + "foobar" | "foobar" + end + + with_them do + it "replace line breaks with an empty string" do + expect(described_class.remove_line_breaks(original)).to eq(expected) + end + end + end + describe '.to_boolean' do it 'accepts booleans' do expect(to_boolean(true)).to be(true) diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 48a67773de9..2c1146ceff5 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -49,4 +49,40 @@ describe Gitlab::VisibilityLevel do .to eq([Gitlab::VisibilityLevel::PUBLIC]) end end + + describe '.allowed_levels' do + it 'only includes the levels that arent restricted' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + + expect(described_class.allowed_levels) + .to contain_exactly(described_class::PRIVATE, described_class::PUBLIC) + end + + it 'returns all levels when no visibility level was set' do + allow(described_class) + .to receive_message_chain('current_application_settings.restricted_visibility_levels') + .and_return(nil) + + expect(described_class.allowed_levels) + .to contain_exactly(described_class::PRIVATE, described_class::INTERNAL, described_class::PUBLIC) + end + end + + describe '.closest_allowed_level' do + it 'picks INTERNAL instead of PUBLIC if public is restricted' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + + expect(described_class.closest_allowed_level(described_class::PUBLIC)) + .to eq(described_class::INTERNAL) + end + + it 'picks PRIVATE if nothing is available' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PRIVATE]) + + expect(described_class.closest_allowed_level(described_class::PUBLIC)) + .to eq(described_class::PRIVATE) + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 249c77dc636..2e7a0265a0b 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -26,11 +26,16 @@ describe Gitlab::Workhorse do 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys ) end + let(:cache_disabled) { false } subject do described_class.send_git_archive(repository, ref: ref, format: format) end + before do + allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled) + end + context 'when Gitaly workhorse_archive feature is enabled' do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -39,6 +44,15 @@ describe Gitlab::Workhorse do expect(command).to eq('git-archive') expect(params).to include(gitaly_params) end + + context 'when archive caching is disabled' do + let(:cache_disabled) { true } + + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) + end + end end context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index ecb4034ec8b..f65e41dfea3 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -50,6 +50,30 @@ describe GoogleApi::CloudPlatform::Client do end end + describe '#projects_list' do + subject { client.projects_list } + let(:projects) { double } + + before do + allow_any_instance_of(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService) + .to receive(:fetch_all).and_return(projects) + end + + it { is_expected.to eq(projects) } + end + + describe '#projects_get_billing_info' do + subject { client.projects_get_billing_info('project') } + let(:billing_info) { double } + + before do + allow_any_instance_of(Google::Apis::CloudbillingV1::CloudbillingService) + .to receive(:get_project_billing_info).and_return(billing_info) + end + + it { is_expected.to eq(billing_info) } + end + describe '#projects_zones_clusters_get' do subject { client.projects_zones_clusters_get(spy, spy, spy) } let(:gke_cluster) { double } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 4d0a3942996..59eda025108 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -71,6 +71,18 @@ describe Notify do is_expected.to have_html_escaped_body_text issue.description end + it 'does not add a reason header' do + is_expected.not_to have_header('X-GitLab-NotificationReason', /.+/) + end + + context 'when sent with a reason' do + subject { described_class.new_issue_email(issue.assignees.first.id, issue.id, NotificationReason::ASSIGNED) } + + it 'includes the reason in a header' do + is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + end + context 'when enabled email_author_in_body' do before do stub_application_setting(email_author_in_body: true) @@ -108,6 +120,14 @@ describe Notify do is_expected.to have_body_text(project_issue_path(project, issue)) end end + + context 'when sent with a reason' do + subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) } + + it 'includes the reason in a header' do + is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + end end describe 'that have been relabeled' do @@ -226,6 +246,14 @@ describe Notify do is_expected.to have_html_escaped_body_text merge_request.description end + context 'when sent with a reason' do + subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) } + + it 'includes the reason in a header' do + is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + end + context 'when enabled email_author_in_body' do before do stub_application_setting(email_author_in_body: true) @@ -263,6 +291,27 @@ describe Notify do is_expected.to have_html_escaped_body_text(assignee.name) end end + + context 'when sent with a reason' do + subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) } + + it 'includes the reason in a header' do + is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + + it 'includes the reason in the footer' do + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED) + is_expected.to have_body_text(text) + + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED) + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED) + expect(new_subject).to have_body_text(text) + + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil) + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil) + expect(new_subject).to have_body_text(text) + end + end end describe 'that have been relabeled' do @@ -417,7 +466,7 @@ describe Notify do context 'for a project in a user namespace' do let(:project) do create(:project, :public, :access_requestable) do |project| - project.team << [project.owner, :master, project.owner] + project.add_master(project.owner, current_user: project.owner) end end @@ -520,7 +569,7 @@ describe Notify do end describe 'project invitation' do - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:master) { create(:user).tap { |u| project.add_master(u) } } let(:project_member) { invite_to_project(project, inviter: master) } subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) } @@ -540,7 +589,7 @@ describe Notify do describe 'project invitation accepted' do let(:invited_user) { create(:user, name: 'invited user') } - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:master) { create(:user).tap { |u| project.add_master(u) } } let(:project_member) do invitee = invite_to_project(project, inviter: master) invitee.accept_invite!(invited_user) @@ -563,7 +612,7 @@ describe Notify do end describe 'project invitation declined' do - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:master) { create(:user).tap { |u| project.add_master(u) } } let(:project_member) do invitee = invite_to_project(project, inviter: master) invitee.decline_invite! @@ -1308,7 +1357,7 @@ describe Notify do matcher :have_part_with do |expected| match do |actual| - actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) } + actual.body.parts.any? { |part| part.content_type.try(:match, /#{expected}/) } end end end diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb new file mode 100644 index 00000000000..4a22bd6f342 --- /dev/null +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_todos.rb') + +describe AddForeignKeysToTodos, :migration do + let(:todos) { table(:todos) } + + let(:project) { create(:project) } + let(:user) { create(:user) } + + context 'add foreign key on user_id' do + let!(:todo_with_user) { create_todo(user_id: user.id) } + let!(:todo_without_user) { create_todo(user_id: 4711) } + + it 'removes orphaned todos without corresponding user' do + expect { migrate! }.to change { Todo.count }.from(2).to(1) + end + + it 'does not remove entries with valid user_id' do + expect { migrate! }.not_to change { todo_with_user.reload } + end + end + + context 'add foreign key on author_id' do + let!(:todo_with_author) { create_todo(author_id: user.id) } + let!(:todo_with_invalid_author) { create_todo(author_id: 4711) } + + it 'removes orphaned todos by author_id' do + expect { migrate! }.to change { Todo.count }.from(2).to(1) + end + + it 'does not touch author_id for valid entries' do + expect { migrate! }.not_to change { todo_with_author.reload } + end + end + + context 'add foreign key on note_id' do + let(:note) { create(:note) } + let!(:todo_with_note) { create_todo(note_id: note.id) } + let!(:todo_with_invalid_note) { create_todo(note_id: 4711) } + let!(:todo_without_note) { create_todo(note_id: nil) } + + it 'deletes todo if note_id is set but does not exist in notes table' do + expect { migrate! }.to change { Todo.count }.from(3).to(2) + end + + it 'does not touch entry if note_id is nil' do + expect { migrate! }.not_to change { todo_without_note.reload } + end + + it 'does not touch note_id for valid entries' do + expect { migrate! }.not_to change { todo_with_note.reload } + end + end + + def create_todo(**opts) + todos.create!( + project_id: project.id, + user_id: user.id, + author_id: user.id, + target_type: '', + action: 0, + state: '', **opts + ) + end +end diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 84c2e9f7e52..63defcb39bf 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') -describe AddHeadPipelineForEachMergeRequest, :truncate do +describe AddHeadPipelineForEachMergeRequest, :delete do include ProjectForksHelper let(:migration) { described_class.new } diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb index 597d8eab51c..f3a46025376 100644 --- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb +++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb') -describe CalculateConvDevIndexPercentages, truncate: true do +describe CalculateConvDevIndexPercentages, :delete do let(:migration) { described_class.new } let!(:conv_dev_index) do create(:conversational_development_index_metric, diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb new file mode 100644 index 00000000000..0258860d169 --- /dev/null +++ b/spec/migrations/clean_up_for_members_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb') + +describe CleanUpForMembers, :migration do + let(:migration) { described_class.new } + let!(:group_member) { create_group_member } + let!(:unbinded_group_member) { create_group_member } + let!(:invited_group_member) { create_group_member(true) } + let!(:not_valid_group_member) { create_group_member } + let!(:project_member) { create_project_member } + let!(:invited_project_member) { create_project_member(true) } + let!(:unbinded_project_member) { create_project_member } + let!(:not_valid_project_member) { create_project_member } + + it 'removes members without proper user_id' do + unbinded_group_member.update_column(:user_id, nil) + not_valid_group_member.update_column(:user_id, 9999) + unbinded_project_member.update_column(:user_id, nil) + not_valid_project_member.update_column(:user_id, 9999) + + migrate! + + expect(Member.all).not_to include(unbinded_group_member, not_valid_group_member, unbinded_project_member, not_valid_project_member) + expect(Member.all).to include(group_member, invited_group_member, project_member, invited_project_member) + end + + def create_group_member(invited = false) + fill_member(GroupMember.new(group: create_group), invited) + end + + def create_project_member(invited = false) + fill_member(ProjectMember.new(project: create_project), invited) + end + + def fill_member(member_object, invited) + member_object.tap do |m| + m.access_level = 40 + m.notification_level = 3 + + if invited + m.user_id = nil + m.invite_token = 'xxx' + m.invite_email = 'email@email.com' + else + m.user_id = create_user.id + end + + m.save + end + + member_object + end + + def create_group + name = FFaker::Lorem.characters(10) + + Group.create(name: name, path: name.downcase.gsub(/\s/, '_')) + end + + def create_project + name = FFaker::Lorem.characters(10) + creator = create_user + + Project.create(name: name, + path: name.downcase.gsub(/\s/, '_'), + namespace: creator.namespace, + creator: creator) + end + + def create_user + User.create(email: FFaker::Internet.email, + password: '12345678', + name: FFaker::Name.name, + username: FFaker::Internet.user_name, + confirmed_at: Time.now, + confirmation_token: nil) + end +end diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb index 1df2477da51..8a191bd7139 100644 --- a/spec/migrations/delete_conflicting_redirect_routes_spec.rb +++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb @@ -10,9 +10,6 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do end before do - stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2) - stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2) - routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') @@ -32,27 +29,14 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') end - it 'correctly schedules background migrations' do + # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252 + it 'NO-OP: does not schedule any background migrations' do Sidekiq::Testing.fake! do Timecop.freeze do migrate! - expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) - expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs.size).to eq 3 + expect(BackgroundMigrationWorker.jobs.size).to eq 0 end end end - - it 'schedules background migrations' do - Sidekiq::Testing.inline! do - expect do - migrate! - end.to change { redirect_routes.count }.from(8).to(3) - end - end end diff --git a/spec/migrations/fix_wrongly_renamed_routes_spec.rb b/spec/migrations/fix_wrongly_renamed_routes_spec.rb index 5ef10b92a3a..543cf55f076 100644 --- a/spec/migrations/fix_wrongly_renamed_routes_spec.rb +++ b/spec/migrations/fix_wrongly_renamed_routes_spec.rb @@ -1,29 +1,35 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170518231126_fix_wrongly_renamed_routes.rb') -describe FixWronglyRenamedRoutes, truncate: true do +describe FixWronglyRenamedRoutes, :migration do let(:subject) { described_class.new } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:routes_table) { table(:routes) } let(:broken_namespace) do - namespace = create(:group, name: 'apiis') - namespace.route.update_attribute(:path, 'api0is') - namespace + namespaces_table.create!(name: 'apiis', path: 'apiis').tap do |namespace| + routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'api0is', path: 'api0is') + end end + let(:broken_namespace_route) { routes_table.where(source_type: 'Namespace', source_id: broken_namespace.id).first } describe '#wrongly_renamed' do it "includes routes that have names that don't match their namespace" do broken_namespace - _other_namespace = create(:group, name: 'api0') + other_namespace = namespaces_table.create!(name: 'api0', path: 'api0') + routes_table.create!(source_type: 'Namespace', source_id: other_namespace.id, name: 'api0', path: 'api0') expect(subject.wrongly_renamed.map(&:id)) - .to contain_exactly(broken_namespace.route.id) + .to contain_exactly(broken_namespace_route.id) end end describe "#paths_and_corrections" do it 'finds the wrong path and gets the correction from the namespace' do broken_namespace - namespace = create(:group, name: 'uploads-test') - namespace.route.update_attribute(:path, 'uploads0-test') + namespaces_table.create!(name: 'uploads-test', path: 'uploads-test').tap do |namespace| + routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'uploads-test', path: 'uploads0-test') + end expected_result = [ { 'namespace_path' => 'apiis', 'path' => 'api0is' }, @@ -36,38 +42,45 @@ describe FixWronglyRenamedRoutes, truncate: true do describe '#routes_in_namespace_query' do it 'includes only the required routes' do - namespace = create(:group, path: 'hello') - project = create(:project, namespace: namespace) - _other_namespace = create(:group, path: 'hello0') - - result = Route.where(subject.routes_in_namespace_query('hello')) - - expect(result).to contain_exactly(namespace.route, project.route) + namespace = namespaces_table.create!(name: 'hello', path: 'hello') + namespace_route = routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'hello', path: 'hello') + project = projects_table.new(name: 'my-project', path: 'my-project', namespace_id: namespace.id).tap do |project| + project.save!(validate: false) + end + routes_table.create!(source_type: 'Project', source_id: project.id, name: 'my-project', path: 'hello/my-project') + _other_namespace = namespaces_table.create!(name: 'hello0', path: 'hello0') + + result = routes_table.where(subject.routes_in_namespace_query('hello')) + project_route = routes_table.where(source_type: 'Project', source_id: project.id).first + + expect(result).to contain_exactly(namespace_route, project_route) end end describe '#up' do - let(:broken_project) do - project = create(:project, namespace: broken_namespace, path: 'broken-project') - project.route.update_attribute(:path, 'api0is/broken-project') - project - end - it 'renames incorrectly named routes' do - broken_project + broken_project = + projects_table.new(name: 'broken-project', path: 'broken-project', namespace_id: broken_namespace.id).tap do |project| + project.save!(validate: false) + routes_table.create!(source_type: 'Project', source_id: project.id, name: 'broken-project', path: 'api0is/broken-project') + end subject.up - expect(broken_project.route.reload.path).to eq('apiis/broken-project') - expect(broken_namespace.route.reload.path).to eq('apiis') + broken_project_route = routes_table.where(source_type: 'Project', source_id: broken_project.id).first + + expect(broken_project_route.path).to eq('apiis/broken-project') + expect(broken_namespace_route.reload.path).to eq('apiis') end it "doesn't touch namespaces that look like something that should be renamed" do - namespace = create(:group, path: 'api0') + namespaces_table.create!(name: 'apiis', path: 'apiis') + namespace = namespaces_table.create!(name: 'hello', path: 'api0') + namespace_route = routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'hello', path: 'api0') subject.up - expect(namespace.route.reload.path).to eq('api0') + expect(namespace_route.reload.path).to eq('api0') end end end diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb new file mode 100644 index 00000000000..d2eef81f396 --- /dev/null +++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_foreign_key.rb') + +# The schema version has to be far enough in advance to have the +# only_mirror_protected_branches column in the projects table to create a +# project via FactoryBot. +describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do + let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } + let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } + let!(:issue_third) { create(:issue) } + + subject { described_class.new } + + it 'removes the orphaned moved_to_id' do + subject.down + + issue_third.update_attributes(moved_to_id: 100000) + + subject.up + + expect(issue_first.reload.moved_to_id).to eq(issue_second.id) + expect(issue_second.reload.moved_to_id).to eq(issue_third.id) + expect(issue_third.reload.moved_to_id).to be_nil + end +end diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb index 57ee2adaaff..c81ec887ded 100644 --- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb +++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb @@ -33,7 +33,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do let(:encrypted_gcp_token) { "'encrypted_gcp_token'" } let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" } - let(:cluster) { Clusters::Cluster.last } + let(:cluster) { described_class::Cluster.last } let(:cluster_id) { cluster.id } before do @@ -46,12 +46,12 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do it 'correctly migrate to new clusters architectures' do migrate! - expect(Clusters::Cluster.count).to eq(1) - expect(Clusters::Project.count).to eq(1) - expect(Clusters::Providers::Gcp.count).to eq(1) - expect(Clusters::Platforms::Kubernetes.count).to eq(1) + expect(described_class::Cluster.count).to eq(1) + expect(described_class::ClustersProject.count).to eq(1) + expect(described_class::ProvidersGcp.count).to eq(1) + expect(described_class::PlatformsKubernetes.count).to eq(1) - expect(cluster.user).to eq(user) + expect(cluster.user_id).to eq(user.id) expect(cluster.enabled).to be_truthy expect(cluster.name).to eq(gcp_cluster_name.delete!("'")) expect(cluster.provider_type).to eq('gcp') @@ -59,7 +59,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.project_ids).to include(project.id) - expect(cluster.provider_gcp.cluster).to eq(cluster) + expect(cluster.provider_gcp.cluster_id).to eq(cluster.id) expect(cluster.provider_gcp.status).to eq(status) expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason)) expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id)) @@ -71,7 +71,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token)) expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv)) - expect(cluster.platform_kubernetes.cluster).to eq(cluster) + expect(cluster.platform_kubernetes.cluster_id).to eq(cluster.id) expect(cluster.platform_kubernetes.api_url).to be_nil expect(cluster.platform_kubernetes.ca_cert).to be_nil expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace)) @@ -109,7 +109,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do let(:encrypted_gcp_token) { "'encrypted_gcp_token'" } let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" } - let(:cluster) { Clusters::Cluster.last } + let(:cluster) { described_class::Cluster.last } let(:cluster_id) { cluster.id } before do @@ -122,12 +122,12 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do it 'correctly migrate to new clusters architectures' do migrate! - expect(Clusters::Cluster.count).to eq(1) - expect(Clusters::Project.count).to eq(1) - expect(Clusters::Providers::Gcp.count).to eq(1) - expect(Clusters::Platforms::Kubernetes.count).to eq(1) + expect(described_class::Cluster.count).to eq(1) + expect(described_class::ClustersProject.count).to eq(1) + expect(described_class::ProvidersGcp.count).to eq(1) + expect(described_class::PlatformsKubernetes.count).to eq(1) - expect(cluster.user).to eq(user) + expect(cluster.user_id).to eq(user.id) expect(cluster.enabled).to be_truthy expect(cluster.name).to eq(tr(gcp_cluster_name)) expect(cluster.provider_type).to eq('gcp') @@ -135,7 +135,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.project_ids).to include(project.id) - expect(cluster.provider_gcp.cluster).to eq(cluster) + expect(cluster.provider_gcp.cluster_id).to eq(cluster.id) expect(cluster.provider_gcp.status).to eq(status) expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason)) expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id)) @@ -147,7 +147,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token)) expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv)) - expect(cluster.platform_kubernetes.cluster).to eq(cluster) + expect(cluster.platform_kubernetes.cluster_id).to eq(cluster.id) expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint)) expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert)) expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace)) diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb index cfd4021fbac..ff0d44e1ed2 100644 --- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb +++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb @@ -8,10 +8,10 @@ describe MigrateIssuesToGhostUser, :migration do let(:users) { table(:users) } before do - projects.create!(name: 'gitlab') + project = projects.create!(name: 'gitlab') user = users.create(email: 'test@example.com') - issues.create(title: 'Issue 1', author_id: nil, project_id: 1) - issues.create(title: 'Issue 2', author_id: user.id, project_id: 1) + issues.create(title: 'Issue 1', author_id: nil, project_id: project.id) + issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id) end context 'when ghost user exists' do diff --git a/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb new file mode 100644 index 00000000000..df0015b6dd3 --- /dev/null +++ b/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb @@ -0,0 +1,312 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb') + +describe MigrateKubernetesServiceToNewClustersArchitectures, :migration do + context 'when unique KubernetesService exists' do + shared_examples 'KubernetesService migration' do + let(:sample_num) { 2 } + + let(:projects) do + (1..sample_num).each_with_object([]) do |n, array| + array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create! + end + end + + let!(:kubernetes_services) do + projects.map do |project| + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: active, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://kubernetes#{project.id}.com\",\"ca_pem\":\"ca_pem#{project.id}\",\"token\":\"token#{project.id}\"}") + end + end + + it 'migrates the KubernetesService to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num) + + projects.each do |project| + project.clusters.last.tap do |cluster| + expect(cluster.enabled).to eq(active) + expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url) + expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem) + expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token) + expect(project.kubernetes_service).not_to be_active + end + end + end + end + + context 'when KubernetesService is active' do + let(:active) { true } + + it_behaves_like 'KubernetesService migration' + end + end + + context 'when unique KubernetesService spawned from Service Template' do + let(:sample_num) { 2 } + + let(:projects) do + (1..sample_num).each_with_object([]) do |n, array| + array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create! + end + end + + let!(:kubernetes_service_template) do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}") + end + + let!(:kubernetes_services) do + projects.map do |project| + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"#{kubernetes_service_template.api_url}\",\"ca_pem\":\"#{kubernetes_service_template.ca_pem}\",\"token\":\"#{kubernetes_service_template.token}\"}") + end + end + + it 'migrates the KubernetesService to Platform::Kubernetes without template' do + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num) + + projects.each do |project| + project.clusters.last.tap do |cluster| + expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url) + expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem) + expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token) + expect(project.kubernetes_service).not_to be_active + end + end + end + end + + context 'when managed KubernetesService exists' do + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + let(:cluster) do + MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!( + projects: [project], + name: 'sample-cluster', + platform_type: :kubernetes, + provider_type: :user, + platform_kubernetes_attributes: { + api_url: 'https://sample.kubernetes.com', + ca_cert: 'ca_pem-sample', + token: 'token-sample' + } ) + end + + let!(:kubernetes_service) do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: cluster.enabled, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"api_url\":\"#{cluster.platform_kubernetes.api_url}\"}") + end + + it 'does not migrate the KubernetesService and disables the kubernetes_service' do # Because the corresponding Platform::Kubernetes already exists + expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count } + + kubernetes_service.reload + expect(kubernetes_service).not_to be_active + end + end + + context 'when production cluster has already been existed' do # i.e. There are no environment_scope conflicts + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + let(:cluster) do + MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!( + projects: [project], + name: 'sample-cluster', + platform_type: :kubernetes, + provider_type: :user, + environment_scope: 'production/*', + platform_kubernetes_attributes: { + api_url: 'https://sample.kubernetes.com', + ca_cert: 'ca_pem-sample', + token: 'token-sample' + } ) + end + + let!(:kubernetes_service) do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"api_url\":\"https://debug.kube.com\"}") + end + + it 'migrates the KubernetesService to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1) + + kubernetes_service.reload + project.clusters.last.tap do |cluster| + expect(cluster.environment_scope).to eq('*') + expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url) + expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem) + expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token) + expect(kubernetes_service).not_to be_active + end + end + end + + context 'when default cluster has already been existed' do + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + let!(:cluster) do + MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!( + projects: [project], + name: 'sample-cluster', + platform_type: :kubernetes, + provider_type: :user, + environment_scope: '*', + platform_kubernetes_attributes: { + api_url: 'https://sample.kubernetes.com', + ca_cert: 'ca_pem-sample', + token: 'token-sample' + } ) + end + + let!(:kubernetes_service) do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"api_url\":\"https://debug.kube.com\"}") + end + + it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1) + + kubernetes_service.reload + project.clusters.last.tap do |cluster| + expect(cluster.environment_scope).to eq('migrated/*') + expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url) + expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem) + expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token) + expect(kubernetes_service).not_to be_active + end + end + end + + context 'when default cluster and migrated cluster has already been existed' do + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + let!(:cluster) do + MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!( + projects: [project], + name: 'sample-cluster', + platform_type: :kubernetes, + provider_type: :user, + environment_scope: '*', + platform_kubernetes_attributes: { + api_url: 'https://sample.kubernetes.com', + ca_cert: 'ca_pem-sample', + token: 'token-sample' + } ) + end + + let!(:migrated_cluster) do + MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!( + projects: [project], + name: 'sample-cluster', + platform_type: :kubernetes, + provider_type: :user, + environment_scope: 'migrated/*', + platform_kubernetes_attributes: { + api_url: 'https://sample.kubernetes.com', + ca_cert: 'ca_pem-sample', + token: 'token-sample' + } ) + end + + let!(:kubernetes_service) do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"api_url\":\"https://debug.kube.com\"}") + end + + it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1) + + kubernetes_service.reload + project.clusters.last.tap do |cluster| + expect(cluster.environment_scope).to eq('migrated0/*') + expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url) + expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem) + expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token) + expect(kubernetes_service).not_to be_active + end + end + end + + context 'when KubernetesService has nullified parameters' do + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + before do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: false, + category: 'deployment', + type: 'KubernetesService', + properties: "{}") + end + + it 'does not migrate the KubernetesService and disables the kubernetes_service' do + expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count } + + expect(project.kubernetes_service).not_to be_active + end + end + + # Platforms::Kubernetes validates `token` reagdless of the activeness, + # whereas KubernetesService validates `token` if only it's activated + # However, in this migration file, there are no validations because of the re-defined model class + # therefore, we should safely add this raw to Platform::Kubernetes + context 'when KubernetesService has empty token' do + let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + before do + MigrateKubernetesServiceToNewClustersArchitectures::Service.create!( + project: project, + active: false, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"http://111.111.111.111\",\"ca_pem\":\"a\",\"token\":\"\"}") + end + + it 'does not migrate the KubernetesService and disables the kubernetes_service' do + expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1) + + project.clusters.last.tap do |cluster| + expect(cluster.environment_scope).to eq('*') + expect(cluster.platform_kubernetes.namespace).to eq('prod') + expect(cluster.platform_kubernetes.api_url).to eq('http://111.111.111.111') + expect(cluster.platform_kubernetes.ca_cert).to eq('a') + expect(cluster.platform_kubernetes.token).to be_empty + expect(project.kubernetes_service).not_to be_active + end + end + end + + context 'when KubernetesService does not exist' do + let!(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! } + + it 'does not migrate the KubernetesService' do + expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count } + end + end +end diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb index 9b92f4b70b0..a837498e1b1 100644 --- a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb +++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb @@ -35,9 +35,9 @@ describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 1, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 3, 3) - expect(described_class::MIGRATION).to be_scheduled_migration(4.minutes, 4, 5) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 3, 3) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 4, 5) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb index 094c9bc604e..79d2708f9ad 100644 --- a/spec/migrations/migrate_stages_statuses_spec.rb +++ b/spec/migrations/migrate_stages_statuses_spec.rb @@ -50,9 +50,9 @@ describe MigrateStagesStatuses, :migration do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 3, 3) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb index 063829be546..a17c9c72bde 100644 --- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb +++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb') -describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :truncate do +describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do let(:migration) { described_class.new } let!(:user_active_1) { create(:user) } let!(:user_active_2) { create(:user) } diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb index 5e16769d63a..31d16e17d7b 100644 --- a/spec/migrations/migrate_user_project_view_spec.rb +++ b/spec/migrations/migrate_user_project_view_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb') -describe MigrateUserProjectView, :truncate do +describe MigrateUserProjectView, :delete do let(:migration) { described_class.new } let!(:user) { create(:user, project_view: 'readme') } diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb index 262d7742aaf..56a78f52802 100644 --- a/spec/migrations/normalize_ldap_extern_uids_spec.rb +++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb @@ -27,11 +27,11 @@ describe NormalizeLdapExternUids, :migration, :sidekiq do migrate! expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(5.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) - expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(15.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb b/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb new file mode 100644 index 00000000000..0ff98933d5c --- /dev/null +++ b/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20171215113714_populate_can_push_from_deploy_keys_projects.rb') + +describe PopulateCanPushFromDeployKeysProjects, :migration do + let(:migration) { described_class.new } + let(:deploy_keys) { table(:keys) } + let(:deploy_keys_projects) { table(:deploy_keys_projects) } + let(:projects) { table(:projects) } + + before do + deploy_keys.inheritance_column = nil + + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') + (1..10).each do |index| + deploy_keys.create!(id: index, title: 'dummy', type: 'DeployKey', key: Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com') + deploy_keys_projects.create!(id: index, deploy_key_id: index, project_id: 1) + end + end + + describe '#up' do + it 'migrates can_push from deploy_keys to deploy_keys_projects' do + deploy_keys.limit(5).update_all(can_push: true) + + expected = deploy_keys.order(:id).pluck(:id, :can_push) + + migration.up + + expect(deploy_keys_projects.order(:id).pluck(:deploy_key_id, :can_push)).to eq expected + end + end + + describe '#down' do + it 'migrates can_push from deploy_keys_projects to deploy_keys' do + deploy_keys_projects.limit(5).update_all(can_push: true) + + expected = deploy_keys_projects.order(:id).pluck(:deploy_key_id, :can_push) + + migration.down + + expect(deploy_keys.order(:id).pluck(:id, :can_push)).to eq expected + end + end +end diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb index e393374028f..e51872239ad 100644 --- a/spec/migrations/remove_duplicate_mr_events_spec.rb +++ b/spec/migrations/remove_duplicate_mr_events_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb') -describe RemoveDuplicateMrEvents, truncate: true do +describe RemoveDuplicateMrEvents, :delete do let(:migration) { described_class.new } describe '#up' do diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb index cf6ae5cda74..7f7ce91378b 100644 --- a/spec/migrations/remove_empty_fork_networks_spec.rb +++ b/spec/migrations/remove_empty_fork_networks_spec.rb @@ -3,12 +3,19 @@ require Rails.root.join('db', 'post_migrate', '20171114104051_remove_empty_fork_ describe RemoveEmptyForkNetworks, :migration do let!(:fork_networks) { table(:fork_networks) } + let!(:projects) { table(:projects) } + let!(:fork_network_members) { table(:fork_network_members) } - let(:deleted_project) { create(:project) } - let!(:empty_network) { create(:fork_network, id: 1, root_project_id: deleted_project.id) } - let!(:other_network) { create(:fork_network, id: 2, root_project_id: create(:project).id) } + let(:deleted_project) { projects.create! } + let!(:empty_network) { fork_networks.create!(id: 1, root_project_id: deleted_project.id) } + let!(:other_network) { fork_networks.create!(id: 2, root_project_id: projects.create.id) } before do + fork_network_members.create(fork_network_id: empty_network.id, + project_id: empty_network.root_project_id) + fork_network_members.create(fork_network_id: other_network.id, + project_id: other_network.root_project_id) + deleted_project.destroy! end diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb new file mode 100644 index 00000000000..d80d61af20b --- /dev/null +++ b/spec/migrations/remove_project_labels_group_id_spec.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb') + +describe RemoveProjectLabelsGroupId, :delete do + let(:migration) { described_class.new } + let(:group) { create(:group) } + let!(:project_label) { create(:label, group_id: group.id) } + let!(:group_label) { create(:group_label) } + + describe '#up' do + it 'updates the project labels group ID' do + expect { migration.up }.to change { project_label.reload.group_id }.to(nil) + end + + it 'keeps the group labels group ID' do + expect { migration.up }.not_to change { group_label.reload.group_id } + end + end +end diff --git a/spec/migrations/remove_soft_removed_objects_spec.rb b/spec/migrations/remove_soft_removed_objects_spec.rb new file mode 100644 index 00000000000..ec089f9106d --- /dev/null +++ b/spec/migrations/remove_soft_removed_objects_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171207150343_remove_soft_removed_objects.rb') + +describe RemoveSoftRemovedObjects, :migration do + describe '#up' do + it 'removes various soft removed objects' do + 5.times do + create_with_deleted_at(:issue) + end + + regular_issue = create(:issue) + + run_migration + + expect(Issue.count).to eq(1) + expect(Issue.first).to eq(regular_issue) + end + + it 'removes the temporary indexes once soft removed data has been removed' do + migration = described_class.new + + run_migration + + disable_migrations_output do + expect(migration.temporary_index_exists?(Issue)).to eq(false) + end + end + + it 'removes routes of soft removed personal namespaces' do + namespace = create_with_deleted_at(:namespace) + group = create(:group) + + expect(Route.where(source: namespace).exists?).to eq(true) + expect(Route.where(source: group).exists?).to eq(true) + + run_migration + + expect(Route.where(source: namespace).exists?).to eq(false) + expect(Route.where(source: group).exists?).to eq(true) + end + + it 'schedules the removal of soft removed groups' do + group = create_with_deleted_at(:group) + admin = create(:user, admin: true) + + expect_any_instance_of(GroupDestroyWorker) + .to receive(:perform) + .with(group.id, admin.id) + + run_migration + end + + it 'does not remove soft removed groups when no admin user could be found' do + create_with_deleted_at(:group) + + expect_any_instance_of(GroupDestroyWorker) + .not_to receive(:perform) + + run_migration + end + end + + def run_migration + disable_migrations_output do + migrate! + end + end + + def create_with_deleted_at(*args) + row = create(*args) + + # We set "deleted_at" this way so we don't run into any column cache issues. + row.class.where(id: row.id).update_all(deleted_at: 1.year.ago) + + row + end +end diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index ae3a4cb9b29..75310075cc5 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserv # This migration uses multiple threads, and thus different transactions. This # means data created in this spec may not be visible to some threads. To work -# around this we use the TRUNCATE cleaning strategy. -describe RenameMoreReservedProjectNames, truncate: true do +# around this we use the DELETE cleaning strategy. +describe RenameMoreReservedProjectNames, :delete do let(:migration) { described_class.new } let!(:project) { create(:project) } diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 462f4c08d63..e6555b1fe6b 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_pr # This migration uses multiple threads, and thus different transactions. This # means data created in this spec may not be visible to some threads. To work -# around this we use the TRUNCATE cleaning strategy. -describe RenameReservedProjectNames, truncate: true do +# around this we use the DELETE cleaning strategy. +describe RenameReservedProjectNames, :delete do let(:migration) { described_class.new } let!(:project) { create(:project) } diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb index 1e9aab3d9a1..cbc0ebeb44d 100644 --- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb +++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170518200835_rename_users_with_renamed_namespace.rb') -describe RenameUsersWithRenamedNamespace, truncate: true do +describe RenameUsersWithRenamedNamespace, :delete do it 'renames a user that had their namespace renamed to the namespace path' do other_user = create(:user, username: 'kodingu') other_user1 = create(:user, username: 'api0') diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb index 0e884a7d910..65ec07da31c 100644 --- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -2,18 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys') describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do - matcher :be_scheduled_migration do |*expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - before do create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb index 76afb6c19cf..d230f064444 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb @@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb index cf323973384..1aab4ae1650 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb @@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(20.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(30.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb index 158d0bc02ed..c9fdbe95d13 100644 --- a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb @@ -44,9 +44,9 @@ describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :si Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, merge_request_1.id, merge_request_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, merge_request_2.id, merge_request_2.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, merge_request_4.id, merge_request_4.id) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb new file mode 100644 index 00000000000..7494624066a --- /dev/null +++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb') + +describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq do + # commits_count attribute is added in a next migration + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + + let!(:mrs) { create_list(:merge_request, 3) } + + it 'correctly schedules background migrations' do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(10.minutes, mrs.first.id, mrs.second.id) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(20.minutes, mrs.third.id, mrs.third.id) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index 7fe7a140e2f..fe4d5b8a279 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -4,18 +4,6 @@ require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_up describe TrackUntrackedUploads, :migration, :sidekiq do include TrackUntrackedUploadsHelpers - matcher :be_scheduled_migration do - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do migrate! diff --git a/spec/migrations/update_retried_for_ci_build_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb index 3742b4dafe5..ccb77766b84 100644 --- a/spec/migrations/update_retried_for_ci_build_spec.rb +++ b/spec/migrations/update_retried_for_ci_build_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170503004427_update_retried_for_ci_build.rb') -describe UpdateRetriedForCiBuild, truncate: true do +describe UpdateRetriedForCiBuild, :delete do let(:pipeline) { create(:ci_pipeline) } let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') } let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') } diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb index d4a1553fb0e..984b428a020 100644 --- a/spec/migrations/update_upload_paths_to_system_spec.rb +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -1,53 +1,59 @@ -require "spec_helper" -require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170317162059_update_upload_paths_to_system.rb') -describe UpdateUploadPathsToSystem do +describe UpdateUploadPathsToSystem, :migration do let(:migration) { described_class.new } + let(:uploads_table) { table(:uploads) } + let(:base_upload_attributes) { { size: 42, uploader: 'John Doe' } } before do allow(migration).to receive(:say) end - describe "#uploads_to_switch_to_new_path" do - it "contains only uploads with the old path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") - group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") + describe '#uploads_to_switch_to_new_path' do + it 'contains only uploads with the old path for the correct models' do + _upload_for_other_type = create_upload('Pipeline', 'uploads/ci_pipeline/avatar.jpg') + _upload_with_system_path = create_upload('Project', 'uploads/-/system/project/avatar.jpg') + _upload_with_other_path = create_upload('Project', 'thelongsecretforafileupload/avatar.jpg') + old_upload = create_upload('Project', 'uploads/project/avatar.jpg') + group_upload = create_upload('Namespace', 'uploads/group/avatar.jpg') - expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) + expect(uploads_table.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) end end - describe "#uploads_to_switch_to_old_path" do - it "contains only uploads with the new path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - _old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + describe '#uploads_to_switch_to_old_path' do + it 'contains only uploads with the new path for the correct models' do + _upload_for_other_type = create_upload('Pipeline', 'uploads/ci_pipeline/avatar.jpg') + upload_with_system_path = create_upload('Project', 'uploads/-/system/project/avatar.jpg') + _upload_with_other_path = create_upload('Project', 'thelongsecretforafileupload/avatar.jpg') + _old_upload = create_upload('Project', 'uploads/project/avatar.jpg') - expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) + expect(uploads_table.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) end end - describe "#up", :truncate do - it "updates old upload records to the new path" do - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + describe '#up' do + it 'updates old upload records to the new path' do + old_upload = create_upload('Project', 'uploads/project/avatar.jpg') migration.up - expect(old_upload.reload.path).to eq("uploads/-/system/project/avatar.jpg") + expect(old_upload.reload.path).to eq('uploads/-/system/project/avatar.jpg') end end - describe "#down", :truncate do - it "updates the new system patsh to the old paths" do - new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") + describe '#down' do + it 'updates the new system patsh to the old paths' do + new_upload = create_upload('Project', 'uploads/-/system/project/avatar.jpg') migration.down - expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") + expect(new_upload.reload.path).to eq('uploads/project/avatar.jpg') end end + + def create_upload(type, path) + uploads_table.create(base_upload_attributes.merge(model_type: type, path: path)) + end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 71aa51e1857..38fb98d4f50 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -34,19 +34,19 @@ describe Ability do end it 'returns false for a guest user' do - project.team << [user, :guest] + project.add_guest(user) expect(described_class.can_edit_note?(user, note)).to be_falsy end it 'returns false for a developer' do - project.team << [user, :developer] + project.add_developer(user) expect(described_class.can_edit_note?(user, note)).to be_falsy end it 'returns true for a master' do - project.team << [user, :master] + project.add_master(user) expect(described_class.can_edit_note?(user, note)).to be_truthy end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 871e8b47650..f5b3b4a9fc5 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -25,6 +25,13 @@ describe Ci::Build do it { is_expected.to be_a(ArtifactMigratable) } + describe 'associations' do + it 'has a bidirectional relationship with projects' do + expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds) + expect(Project.reflect_on_association(:builds).has_inverse?).to eq(:project) + end + end + describe 'callbacks' do context 'when running after_create callback' do it 'triggers asynchronous build hooks worker' do @@ -255,6 +262,42 @@ describe Ci::Build do end end + describe '#cache' do + let(:options) { { cache: { key: "key", paths: ["public"], policy: "pull-push" } } } + + subject { build.cache } + + context 'when build has cache' do + before do + allow(build).to receive(:options).and_return(options) + end + + context 'when project has jobs_cache_index' do + before do + allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) + end + + it { is_expected.to be_an(Array).and all(include(key: "key_1")) } + end + + context 'when project does not have jobs_cache_index' do + before do + allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(nil) + end + + it { is_expected.to eq([options[:cache]]) } + end + end + + context 'when build does not have cache' do + before do + allow(build).to receive(:options).and_return({}) + end + + it { is_expected.to eq([nil]) } + end + end + describe '#depends_on_builds' do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 9a278212efc..8ee15f0e734 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -12,7 +12,6 @@ describe Ci::PipelineSchedule do it { is_expected.to respond_to(:cron_timezone) } it { is_expected.to respond_to(:description) } it { is_expected.to respond_to(:next_run_at) } - it { is_expected.to respond_to(:deleted_at) } describe 'validations' do it 'does not allow invalid cron patters' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index a1f63a2534b..14d234f6aab 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -28,6 +28,13 @@ describe Ci::Pipeline, :mailer do it { is_expected.to respond_to :short_sha } it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } + describe 'associations' do + it 'has a bidirectional relationship with projects' do + expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:pipelines) + expect(Project.reflect_on_association(:pipelines).has_inverse?).to eq(:project) + end + end + describe '#source' do context 'when creating new pipeline' do let(:pipeline) do @@ -1440,7 +1447,7 @@ describe Ci::Pipeline, :mailer do end before do - project.team << [pipeline.user, Gitlab::Access::DEVELOPER] + project.add_developer(pipeline.user) pipeline.user.global_notification_setting .update(level: 'custom', failed_pipeline: true, success_pipeline: true) diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index bd9c837402f..d4b72205203 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -69,7 +69,7 @@ describe Ci::Trigger do context 'and is member of the project' do before do - project.team << [owner, :developer] + project.add_developer(owner) end it { is_expected.to eq(true) } diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index f8855079842..eb57abaf6ef 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -40,13 +40,13 @@ describe Clusters::Applications::Helm do describe '#install_command' do it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true, chart: nil) + expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true) end end describe 'status state machine' do describe '#make_installing' do - subject { create(:cluster_applications_helm, :scheduled) } + subject { create(:clusters_applications_helm, :scheduled) } it 'is installing' do subject.make_installing! @@ -56,7 +56,7 @@ describe Clusters::Applications::Helm do end describe '#make_installed' do - subject { create(:cluster_applications_helm, :installing) } + subject { create(:clusters_applications_helm, :installing) } it 'is installed' do subject.make_installed @@ -66,7 +66,7 @@ describe Clusters::Applications::Helm do end describe '#make_errored' do - subject { create(:cluster_applications_helm, :installing) } + subject { create(:clusters_applications_helm, :installing) } let(:reason) { 'some errors' } it 'is errored' do @@ -78,7 +78,7 @@ describe Clusters::Applications::Helm do end describe '#make_scheduled' do - subject { create(:cluster_applications_helm, :installable) } + subject { create(:clusters_applications_helm, :installable) } it 'is scheduled' do subject.make_scheduled @@ -87,7 +87,7 @@ describe Clusters::Applications::Helm do end describe 'when was errored' do - subject { create(:cluster_applications_helm, :errored) } + subject { create(:clusters_applications_helm, :errored) } it 'clears #status_reason' do expect(subject.status_reason).not_to be_nil diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index b83472e1944..619c088b0bf 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -4,105 +4,5 @@ describe Clusters::Applications::Ingress do it { is_expected.to belong_to(:cluster) } it { is_expected.to validate_presence_of(:cluster) } - describe '#name' do - it 'is .application_name' do - expect(subject.name).to eq(described_class.application_name) - end - - it 'is recorded in Clusters::Cluster::APPLICATIONS' do - expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) - end - end - - describe '#status' do - let(:cluster) { create(:cluster, :provided_by_gcp) } - - subject { described_class.new(cluster: cluster) } - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - - context 'when application helm is scheduled' do - before do - create(:cluster_applications_helm, :scheduled, cluster: cluster) - end - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'when application helm is installed' do - before do - create(:cluster_applications_helm, :installed, cluster: cluster) - end - - it 'defaults to :installable' do - expect(subject.status_name).to be(:installable) - end - end - end - - describe '#install_command' do - it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false, chart: subject.chart) - end - end - - describe 'status state machine' do - describe '#make_installing' do - subject { create(:cluster_applications_ingress, :scheduled) } - - it 'is installing' do - subject.make_installing! - - expect(subject).to be_installing - end - end - - describe '#make_installed' do - subject { create(:cluster_applications_ingress, :installing) } - - it 'is installed' do - subject.make_installed - - expect(subject).to be_installed - end - end - - describe '#make_errored' do - subject { create(:cluster_applications_ingress, :installing) } - let(:reason) { 'some errors' } - - it 'is errored' do - subject.make_errored(reason) - - expect(subject).to be_errored - expect(subject.status_reason).to eq(reason) - end - end - - describe '#make_scheduled' do - subject { create(:cluster_applications_ingress, :installable) } - - it 'is scheduled' do - subject.make_scheduled - - expect(subject).to be_scheduled - end - - describe 'when was errored' do - subject { create(:cluster_applications_ingress, :errored) } - - it 'clears #status_reason' do - expect(subject.status_reason).not_to be_nil - - subject.make_scheduled! - - expect(subject.status_reason).to be_nil - end - end - end - end + include_examples 'cluster application specs', described_class end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb new file mode 100644 index 00000000000..696099f7cf7 --- /dev/null +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe Clusters::Applications::Prometheus do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to validate_presence_of(:cluster) } + + include_examples 'cluster application specs', described_class + + describe "#chart_values_file" do + subject { create(:clusters_applications_prometheus).chart_values_file } + + it 'should return chart values file path' do + expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") + end + end +end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 2683d21ddbe..799d7ced116 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -5,6 +5,9 @@ describe Clusters::Cluster do it { is_expected.to have_many(:projects) } it { is_expected.to have_one(:provider_gcp) } it { is_expected.to have_one(:platform_kubernetes) } + it { is_expected.to have_one(:application_helm) } + it { is_expected.to have_one(:application_ingress) } + it { is_expected.to have_one(:application_prometheus) } it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } @@ -190,11 +193,12 @@ describe Clusters::Cluster do end context 'when applications are created' do - let!(:helm) { create(:cluster_applications_helm, cluster: cluster) } - let!(:ingress) { create(:cluster_applications_ingress, cluster: cluster) } + let!(:helm) { create(:clusters_applications_helm, cluster: cluster) } + let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) } + let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } it 'returns a list of created applications' do - is_expected.to contain_exactly(helm, ingress) + is_expected.to contain_exactly(helm, ingress, prometheus) end end end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 38829773599..f2efcd9d0e9 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -151,11 +151,11 @@ describe CommitRange do .with(commit1, user) .and_return(true) - expect(commit1.has_been_reverted?(user, issue)).to eq(true) + expect(commit1.has_been_reverted?(user, issue.notes_with_associations)).to eq(true) end - it 'returns false a commit has not been reverted' do - expect(commit1.has_been_reverted?(user, issue)).to eq(false) + it 'returns false if the commit has not been reverted' do + expect(commit1.has_been_reverted?(user, issue.notes_with_associations)).to eq(false) end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index cd955a5eb69..959383ff0b7 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -181,7 +181,6 @@ eos it { is_expected.to respond_to(:parents) } it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:diffs) } - it { is_expected.to respond_to(:tree) } it { is_expected.to respond_to(:id) } it { is_expected.to respond_to(:to_patch) } end @@ -193,8 +192,8 @@ eos let(:commiter) { create :user } before do - project.team << [commiter, :developer] - other_project.team << [commiter, :developer] + project.add_developer(commiter) + other_project.add_developer(commiter) end it 'detects issues that this commit is marked as closing' do @@ -229,7 +228,7 @@ eos it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') } it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') } - it { expect(data[:added]).to eq(["bar/branch-test.txt"]) } + it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") } it { expect(data[:modified]).to eq([]) } it { expect(data[:removed]).to eq([]) } end @@ -440,15 +439,25 @@ eos end describe '#uri_type' do - it 'returns the URI type at the given path' do - expect(commit.uri_type('files/html')).to be(:tree) - expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) - expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) - expect(commit.uri_type('files/js/application.js')).to be(:blob) + shared_examples 'URI type' do + it 'returns the URI type at the given path' do + expect(commit.uri_type('files/html')).to be(:tree) + expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) + expect(commit.uri_type('files/js/application.js')).to be(:blob) + end + + it "returns nil if the path doesn't exists" do + expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + end + end + + context 'when Gitaly commit_tree_entry feature is enabled' do + it_behaves_like 'URI type' end - it "returns nil if the path doesn't exists" do - expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + context 'when Gitaly commit_tree_entry feature is disabled', :disable_gitaly do + it_behaves_like 'URI type' end end @@ -514,4 +523,17 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end + + describe '#merge_requests' do + let!(:project) { create(:project, :repository) } + let!(:merge_request1) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } + let!(:merge_request2) { create(:merge_request, source_project: project, source_branch: 'merged-target', target_branch: 'feature') } + let(:commit1) { merge_request1.merge_request_diff.commits.last } + let(:commit2) { merge_request1.merge_request_diff.commits.first } + + it 'returns merge_requests that introduced that commit' do + expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2) + expect(commit2.merge_requests).to contain_exactly(merge_request1) + end + end end diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb index cbdc438be0b..3696e6f62fd 100644 --- a/spec/models/concerns/avatarable_spec.rb +++ b/spec/models/concerns/avatarable_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe Avatarable do - subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } let(:gitlab_host) { "https://gitlab.example.com" } let(:relative_url_root) { "/gitlab" } - let(:asset_host) { "https://gitlab-assets.example.com" } + let(:asset_host) { 'https://gitlab-assets.example.com' } before do stub_config_setting(base_url: gitlab_host) @@ -15,29 +15,32 @@ describe Avatarable do describe '#avatar_path' do using RSpec::Parameterized::TableSyntax - where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do - true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::PUBLIC | true | [subject.avatar.url] - true | Project::PUBLIC | false | [asset_host, subject.avatar.url] - false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url] - false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url] - false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url] - false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url] - false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url] - false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url] + where(:has_asset_host, :visibility_level, :only_path, :avatar_path_prefix) do + true | Project::PRIVATE | true | [gitlab_host, relative_url_root] + true | Project::PRIVATE | false | [gitlab_host, relative_url_root] + true | Project::INTERNAL | true | [gitlab_host, relative_url_root] + true | Project::INTERNAL | false | [gitlab_host, relative_url_root] + true | Project::PUBLIC | true | [] + true | Project::PUBLIC | false | [asset_host] + false | Project::PRIVATE | true | [relative_url_root] + false | Project::PRIVATE | false | [gitlab_host, relative_url_root] + false | Project::INTERNAL | true | [relative_url_root] + false | Project::INTERNAL | false | [gitlab_host, relative_url_root] + false | Project::PUBLIC | true | [relative_url_root] + false | Project::PUBLIC | false | [gitlab_host, relative_url_root] end with_them do before do - allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil) - subject.visibility_level = visibility_level + allow(ActionController::Base).to receive(:asset_host) { has_asset_host && asset_host } + + project.visibility_level = visibility_level end + let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join } + it 'returns the expected avatar path' do - expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join) + expect(project.avatar_path(only_path: only_path)).to eq(avatar_path) end end end diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb new file mode 100644 index 00000000000..7bb89fe41dc --- /dev/null +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +describe DeploymentPlatform do + let(:project) { create(:project) } + + describe '#deployment_platform' do + subject { project.deployment_platform } + + context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and a Kubernetes template configured' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + it 'returns a platform kubernetes' do + expect(subject).to be_a_kind_of(Clusters::Platforms::Kubernetes) + end + + it 'creates a cluster and a platform kubernetes' do + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Platforms::Kubernetes.count }.by(1) + end + + it 'includes appropriate attributes for Cluster' do + cluster = subject.cluster + expect(cluster.name).to eq('kubernetes-template') + expect(cluster.project).to eq(project) + expect(cluster.provider_type).to eq('user') + expect(cluster.platform_type).to eq('kubernetes') + end + + it 'creates a platform kubernetes' do + expect { subject }.to change { Clusters::Platforms::Kubernetes.count }.by(1) + end + + it 'copies attributes from Clusters::Platform::Kubernetes template into the new Cluster::Platforms::Kubernetes' do + expect(subject.api_url).to eq(kubernetes_service.api_url) + expect(subject.ca_pem).to eq(kubernetes_service.ca_pem) + expect(subject.token).to eq(kubernetes_service.token) + expect(subject.namespace).to eq(kubernetes_service.namespace) + end + end + + context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and no Kubernetes template configured' do + it { is_expected.to be_nil } + end + + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let(:platform_kubernetes) { cluster.platform_kubernetes } + + it 'returns the Kubernetes platform' do + expect(subject).to eq(platform_kubernetes) + end + end + + context 'when user configured kubernetes integration from project services' do + let!(:kubernetes_service) { create(:kubernetes_service, project: project) } + + it 'returns the Kubernetes service' do + expect(subject).to eq(kubernetes_service) + end + end + + context 'when the cluster creation fails' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + before do + allow_any_instance_of(Clusters::Cluster).to receive(:persisted?).and_return(false) + end + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb index 2322eb206fb..30572ce9332 100644 --- a/spec/models/concerns/discussion_on_diff_spec.rb +++ b/spec/models/concerns/discussion_on_diff_spec.rb @@ -20,6 +20,16 @@ describe DiscussionOnDiff do expect(truncated_lines).not_to include(be_meta) end end + + context "when the diff line does not exist on a legacy diff note" do + it "returns an empty array" do + legacy_note = LegacyDiffNote.new + + allow(subject).to receive(:first_note).and_return(legacy_note) + + expect(truncated_lines).to eq([]) + end + end end describe '#line_code_in_diffs' do diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 8b545aec7f5..c73ea6aa94c 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -62,7 +62,7 @@ describe Issue, "Mentionable" do context 'when the current user can see the issue' do before do - private_project.team << [user, Gitlab::Access::DEVELOPER] + private_project.add_developer(user) end it 'includes the reference' do @@ -107,7 +107,7 @@ describe Issue, "Mentionable" do let(:issues) { create_list(:issue, 2, project: project, author: author) } before do - project.team << [author, Gitlab::Access::DEVELOPER] + project.add_developer(author) end context 'before changes are persisted' do diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 673c609f534..87bf731340f 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -24,8 +24,8 @@ describe Milestone, 'Milestoneish' do let(:label_3) { create(:label, title: 'label_3', project: project) } before do - project.team << [member, :developer] - project.team << [guest, :guest] + project.add_developer(member) + project.add_guest(guest) end describe '#sorted_issues' do diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb index 1616c2ea985..2a2ef5a304d 100644 --- a/spec/models/concerns/resolvable_discussion_spec.rb +++ b/spec/models/concerns/resolvable_discussion_spec.rb @@ -190,7 +190,7 @@ describe Discussion, ResolvableDiscussion do context "when the signed in user can push to the project" do before do - subject.project.team << [current_user, :master] + subject.project.add_master(current_user) end it "returns true" do diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb new file mode 100644 index 00000000000..621d2d38eae --- /dev/null +++ b/spec/models/concerns/triggerable_hooks_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +RSpec.describe TriggerableHooks do + before do + class TestableHook < WebHook + include TriggerableHooks + triggerable_hooks [:push_hooks] + end + end + + describe 'scopes' do + it 'defines a scope for each of the requested triggers' do + expect(TestableHook).to respond_to :push_hooks + expect(TestableHook).not_to respond_to :tag_push_hooks + end + end + + describe '.hooks_for' do + context 'the model has the required trigger scope' do + it 'returns the record' do + hook = TestableHook.create!(url: 'http://example.com', push_events: true) + + expect(TestableHook.hooks_for(:push_hooks)).to eq [hook] + end + end + + context 'the model does not have the required trigger scope' do + it 'returns an empty relation' do + TestableHook.create!(url: 'http://example.com') + + expect(TestableHook.hooks_for(:tag_push_hooks)).to eq [] + end + end + + context 'the stock scope ".all" is accepted' do + it 'returns the record' do + hook = TestableHook.create!(url: 'http://example.com') + + expect(TestableHook.hooks_for(:all)).to eq [hook] + end + end + end +end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 0345fefb254..fca3090ff4a 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -8,7 +8,7 @@ describe DeployKeysProject do describe "Validation" do it { is_expected.to validate_presence_of(:project_id) } - it { is_expected.to validate_presence_of(:deploy_key_id) } + it { is_expected.to validate_presence_of(:deploy_key) } end describe "Destroying" do diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb index fa02434b0fd..50b19000799 100644 --- a/spec/models/diff_discussion_spec.rb +++ b/spec/models/diff_discussion_spec.rb @@ -47,8 +47,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns the diff ID for the version to show' do - expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id) + context 'when commit_id is not present' do + it 'returns the diff ID for the version to show' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'includes the commit_id in the result' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id, commit_id: 'commit_123') + end end end @@ -70,8 +82,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns the diff ID and start sha of the versions to compare' do - expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) + context 'when commit_id is not present' do + it 'returns the diff ID and start sha of the versions to compare' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'includes the commit_id in the result' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha, commit_id: 'commit_123') + end end end @@ -83,8 +107,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns nil' do - expect(subject.merge_request_version_params).to be_nil + context 'when commit_id is not present' do + it 'returns empty hash' do + expect(subject.merge_request_version_params).to eq(nil) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'returns the commit_id' do + expect(subject.merge_request_version_params).to eq(commit_id: 'commit_123') + end end end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 4d0b3245a13..2705421e540 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -112,22 +112,6 @@ describe DiffNote do end end - describe "#for_line?" do - context "when provided the correct diff line" do - it "returns true" do - expect(subject.for_line?(subject.diff_line)).to be true - end - end - - context "when provided a different diff line" do - it "returns false" do - some_line = subject.diff_file.diff_lines.first - - expect(subject.for_line?(some_line)).to be false - end - end - end - describe "#active?" do context "when noteable is a commit" do subject { build(:diff_note_on_commit, project: project, position: position) } diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index aa7a8342a4c..67f49348acb 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -125,8 +125,8 @@ describe Event do let(:event) { described_class.new(project: project, target: target, author_id: author.id) } before do - project.team << [member, :developer] - project.team << [guest, :guest] + project.add_developer(member) + project.add_guest(guest) end context 'commit note event' do @@ -347,6 +347,22 @@ describe Event do end end + describe '#target' do + it 'eager loads the author of an event target' do + create(:closed_issue_event) + + events = described_class.preload(:target).all.to_a + count = ActiveRecord::QueryRecorder + .new { events.first.target.author }.count + + # This expectation exists to make sure the test doesn't pass when the + # author is for some reason not loaded at all. + expect(events.first.target.author).to be_an_instance_of(User) + + expect(count).to be_zero + end + end + def create_push_event(project, user) event = create(:push_event, project: project, author: user) diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 7f1909710d8..673049d1cc4 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -43,7 +43,7 @@ describe GenericCommitStatus do context 'when user has ability to see datails' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'details path points to an external URL' do diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 431e3db9f00..8bc45715dcd 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -7,7 +7,8 @@ describe SystemHook do it 'sets defined default parameters' do attrs = { push_events: false, - repository_update_events: true + repository_update_events: true, + merge_requests_events: false } expect(system_hook).to have_attributes(attrs) end @@ -62,7 +63,7 @@ describe SystemHook do end it "project_create hook" do - project.team << [user, :master] + project.add_master(user) expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_team/, @@ -71,7 +72,7 @@ describe SystemHook do end it "project_destroy hook" do - project.team << [user, :master] + project.add_master(user) project.project_members.destroy_all expect(WebMock).to have_requested(:post, system_hook.url).with( diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 388120160ab..ea6d6e53ef5 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -29,6 +29,12 @@ describe WebHook do expect(hook.url).to eq('https://example.com') end end + + describe 'token' do + it { is_expected.to allow_value("foobar").for(:token) } + + it { is_expected.not_to allow_values("foo\nbar", "foo\r\nbar").for(:token) } + end end describe 'execute' do diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb index 34d98a3c975..580a98193af 100644 --- a/spec/models/issue_collection_spec.rb +++ b/spec/models/issue_collection_spec.rb @@ -42,7 +42,7 @@ describe IssueCollection do context 'using a user that has reporter access to the project' do it 'returns the issues of the project' do - project.team << [user, :reporter] + project.add_reporter(user) expect(collection.updatable_by_user(user)).to eq([issue1, issue2]) end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 0ea287d007a..f5c9f551e65 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -18,9 +18,30 @@ describe Issue do subject { create(:issue) } - describe "act_as_paranoid" do - it { is_expected.to have_db_column(:deleted_at) } - it { is_expected.to have_db_index(:deleted_at) } + describe 'callbacks' do + describe '#ensure_metrics' do + it 'creates metrics after saving' do + issue = create(:issue) + + expect(issue.metrics).to be_persisted + expect(Issue::Metrics.count).to eq(1) + end + + it 'does not create duplicate metrics for an issue' do + issue = create(:issue) + + issue.close! + + expect(issue.metrics).to be_persisted + expect(Issue::Metrics.count).to eq(1) + end + + it 'records current metrics' do + expect_any_instance_of(Issue::Metrics).to receive(:record!) + + create(:issue) + end + end end describe '#order_by_position_and_priority' do @@ -238,7 +259,7 @@ describe Issue do let(:issue) { create(:issue, project: project) } before do - project.team << [user, :reporter] + project.add_reporter(user) end it { is_expected.to eq true } @@ -254,7 +275,7 @@ describe Issue do context 'destination project allowed' do before do - to_project.team << [user, :reporter] + to_project.add_reporter(user) end it { is_expected.to eq true } @@ -262,7 +283,7 @@ describe Issue do context 'destination project not allowed' do before do - to_project.team << [user, :guest] + to_project.add_guest(user) end it { is_expected.to eq false } @@ -550,7 +571,7 @@ describe Issue do context 'when the user is the project owner' do before do - project.team << [user, :master] + project.add_master(user) end it 'returns true for a regular issue' do @@ -574,7 +595,7 @@ describe Issue do context 'using a public project' do before do - project.team << [user, Gitlab::Access::DEVELOPER] + project.add_developer(user) end it 'returns true for a regular issue' do @@ -594,7 +615,7 @@ describe Issue do let(:project) { create(:project, :internal) } before do - project.team << [user, Gitlab::Access::DEVELOPER] + project.add_developer(user) end it 'returns true for a regular issue' do @@ -614,7 +635,7 @@ describe Issue do let(:project) { create(:project, :private) } before do - project.team << [user, Gitlab::Access::DEVELOPER] + project.add_developer(user) end it 'returns true for a regular issue' do diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 4cd9e3f4f1d..bf5703ac986 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -1,13 +1,6 @@ require 'spec_helper' describe Key, :mailer do - include Gitlab::CurrentSettings - - describe 'modules' do - subject { described_class } - it { is_expected.to include_module(Gitlab::CurrentSettings) } - end - describe "Associations" do it { is_expected.to belong_to(:user) } end @@ -79,16 +72,53 @@ describe Key, :mailer do expect(build(:key)).to be_valid end - it 'accepts a key with newline charecters after stripping them' do - key = build(:key) - key.key = key.key.insert(100, "\n") - key.key = key.key.insert(40, "\r\n") - expect(key).to be_valid - end - it 'rejects the unfingerprintable key (not a key)' do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + + where(:factory, :chars, :expected_sections) do + [ + [:key, ["\n", "\r\n"], 3], + [:key, [' ', ' '], 3], + [:key_without_comment, [' ', ' '], 2] + ] + end + + with_them do + let!(:key) { create(factory) } + let!(:original_fingerprint) { key.fingerprint } + + it 'accepts a key with blank space characters after stripping them' do + modified_key = key.key.insert(100, chars.first).insert(40, chars.last) + _, content = modified_key.split + + key.update!(key: modified_key) + + expect(key).to be_valid + expect(key.key.split.size).to eq(expected_sections) + + expect(content).not_to match(/\s/) + expect(original_fingerprint).to eq(key.fingerprint) + end + end + end + + context 'validate size' do + where(:key_content, :result) do + [ + [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true] + ] + end + + with_them do + it 'validates the size of the key' do + key = build(:key, key: key_content) + + expect(key.valid?).to eq(result) + end + end end context 'validate it meets key restrictions' do diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 0a017c068ad..c64cdf8f812 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -62,12 +62,12 @@ describe Member do @owner_user = create(:user).tap { |u| group.add_owner(u) } @owner = group.members.find_by(user_id: @owner_user.id) - @master_user = create(:user).tap { |u| project.team << [u, :master] } + @master_user = create(:user).tap { |u| project.add_master(u) } @master = project.members.find_by(user_id: @master_user.id) @blocked_user = create(:user).tap do |u| - project.team << [u, :master] - project.team << [u, :developer] + project.add_master(u) + project.add_developer(u) u.block! end @@ -488,7 +488,7 @@ describe Member do member.accept_invite!(user) end - it "refreshes user's authorized projects", :truncate do + it "refreshes user's authorized projects", :delete do project = member.source expect(user.authorized_projects).not_to include(project) @@ -523,11 +523,11 @@ describe Member do end end - describe "destroying a record", :truncate do + describe "destroying a record", :delete do it "refreshes user's authorized projects" do project = create(:project, :private) user = create(:user) - member = project.team << [user, :reporter] + member = project.add_reporter(user) member.destroy diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index fa3e80ba062..3e46fa36375 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -88,8 +88,8 @@ describe ProjectMember do @user_1 = create :user @user_2 = create :user - @project_1.team << [@user_1, :developer] - @project_2.team << [@user_2, :reporter] + @project_1.add_developer(@user_1) + @project_2.add_reporter(@user_2) @status = @project_2.team.import(@project_1) end @@ -136,8 +136,8 @@ describe ProjectMember do @user_1 = create :user @user_2 = create :user - @project_1.team << [@user_1, :developer] - @project_2.team << [@user_2, :reporter] + @project_1.add_developer(@user_1) + @project_2.add_reporter(@user_2) described_class.truncate_teams([@project_1.id, @project_2.id]) end diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb index 9353d5c3c8a..02ff7839739 100644 --- a/spec/models/merge_request/metrics_spec.rb +++ b/spec/models/merge_request/metrics_spec.rb @@ -1,16 +1,11 @@ require 'spec_helper' describe MergeRequest::Metrics do - subject { create(:merge_request) } + subject { described_class.new } - describe "when recording the default set of metrics on merge request save" do - it "records the merge time" do - time = Time.now - Timecop.freeze(time) { subject.mark_as_merged } - metrics = subject.metrics - - expect(metrics).to be_present - expect(metrics.merged_at).to be_like_time(time) - end + describe 'associations' do + it { is_expected.to belong_to(:merge_request) } + it { is_expected.to belong_to(:latest_closed_by).class_name('User') } + it { is_expected.to belong_to(:merged_by).class_name('User') } end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index d556004eccf..b4249d72fc8 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -15,6 +15,28 @@ describe MergeRequestDiff do it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') } end + describe '.by_commit_sha' do + subject(:by_commit_sha) { described_class.by_commit_sha(sha) } + + let!(:merge_request) { create(:merge_request, :with_diffs) } + + context 'with sha contained in' do + let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + + it 'returns merge request diffs' do + expect(by_commit_sha).to eq([merge_request.merge_request_diff]) + end + end + + context 'with sha not contained in' do + let(:sha) { 'b83d6e3' } + + it 'returns empty result' do + expect(by_commit_sha).to be_empty + end + end + end + describe '#latest' do let!(:mr) { create(:merge_request, :with_diffs) } let!(:first_diff) { mr.merge_request_diff } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bb63abd167b..243eeddc7a8 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -24,11 +24,6 @@ describe MergeRequest do it { is_expected.to include_module(Taskable) } end - describe "act_as_paranoid" do - it { is_expected.to have_db_column(:deleted_at) } - it { is_expected.to have_db_index(:deleted_at) } - end - describe 'validation' do it { is_expected.to validate_presence_of(:target_branch) } it { is_expected.to validate_presence_of(:source_branch) } @@ -65,6 +60,25 @@ describe MergeRequest do end end + describe 'callbacks' do + describe '#ensure_merge_request_metrics' do + it 'creates metrics after saving' do + merge_request = create(:merge_request) + + expect(merge_request.metrics).to be_persisted + expect(MergeRequest::Metrics.count).to eq(1) + end + + it 'does not duplicate metrics for a merge request' do + merge_request = create(:merge_request) + + merge_request.mark_as_merged! + + expect(MergeRequest::Metrics.count).to eq(1) + end + end + end + describe 'respond to' do it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:can_be_merged?) } @@ -73,6 +87,39 @@ describe MergeRequest do it { is_expected.to respond_to(:merge_when_pipeline_succeeds) } end + describe '.by_commit_sha' do + subject(:by_commit_sha) { described_class.by_commit_sha(sha) } + + let!(:merge_request) { create(:merge_request, :with_diffs) } + + context 'with sha contained in latest merge request diff' do + let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + + it 'returns merge requests' do + expect(by_commit_sha).to eq([merge_request]) + end + end + + context 'with sha contained not in latest merge request diff' do + let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + + it 'returns empty requests' do + latest_merge_request_diff = merge_request.merge_request_diffs.create + latest_merge_request_diff.merge_request_diff_commits.where(sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0').delete_all + + expect(by_commit_sha).to be_empty + end + end + + context 'with sha not contained in' do + let(:sha) { 'b83d6e3' } + + it 'returns empty result' do + expect(by_commit_sha).to be_empty + end + end + end + describe '.in_projects' do it 'returns the merge requests for a set of projects' do expect(described_class.in_projects(Project.all)).to eq([subject]) @@ -195,7 +242,7 @@ describe MergeRequest do describe '#cache_merge_request_closes_issues!' do before do - subject.project.team << [subject.author, :developer] + subject.project.add_developer(subject.author) subject.target_branch = subject.project.default_branch end @@ -481,7 +528,7 @@ describe MergeRequest do let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") } before do - subject.project.team << [subject.author, :developer] + subject.project.add_developer(subject.author) allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end @@ -509,7 +556,7 @@ describe MergeRequest do let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") } it 'detects issues mentioned in description but not closed' do - subject.project.team << [subject.author, :developer] + subject.project.add_developer(subject.author) subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}" allow(subject).to receive(:commits).and_return([commit]) @@ -521,7 +568,7 @@ describe MergeRequest do context 'when the project has an external issue tracker' do before do - subject.project.team << [subject.author, :developer] + subject.project.add_developer(subject.author) commit = double(:commit, safe_message: 'Fixes TEST-3') create(:jira_service, project: subject.project) @@ -660,7 +707,7 @@ describe MergeRequest do it 'includes its closed issues in the body' do issue = create(:issue, project: subject.project) - subject.project.team << [subject.author, :developer] + subject.project.add_developer(subject.author) subject.description = "This issue Closes #{issue.to_reference}" allow(subject.project).to receive(:default_branch) @@ -1016,6 +1063,103 @@ describe MergeRequest do end end + describe '#can_be_reverted?' do + context 'when there is no merge_commit for the MR' do + before do + subject.metrics.update!(merged_at: Time.now.utc) + end + + it 'returns false' do + expect(subject.can_be_reverted?(nil)).to be_falsey + end + end + + context 'when the MR has been merged' do + before do + MergeRequests::MergeService + .new(subject.target_project, subject.author) + .execute(subject) + end + + context 'when there is no revert commit' do + it 'returns true' do + expect(subject.can_be_reverted?(nil)).to be_truthy + end + end + + context 'when there is no merged_at for the MR' do + before do + subject.metrics.update!(merged_at: nil) + end + + it 'returns true' do + expect(subject.can_be_reverted?(nil)).to be_truthy + end + end + + context 'when there is a revert commit' do + let(:current_user) { subject.author } + let(:branch) { subject.target_branch } + let(:project) { subject.target_project } + + let(:revert_commit_id) do + params = { + commit: subject.merge_commit, + branch_name: branch, + start_branch: branch + } + + Commits::RevertService.new(project, current_user, params).execute[:result] + end + + before do + project.add_master(current_user) + + ProcessCommitWorker.new.perform(project.id, + current_user.id, + project.commit(revert_commit_id).to_hash, + project.default_branch == branch) + end + + context 'when the revert commit is mentioned in a note after the MR was merged' do + it 'returns false' do + expect(subject.can_be_reverted?(current_user)).to be_falsey + end + end + + context 'when there is no merged_at for the MR' do + before do + subject.metrics.update!(merged_at: nil) + end + + it 'returns false' do + expect(subject.can_be_reverted?(current_user)).to be_falsey + end + end + + context 'when the revert commit is mentioned in a note just before the MR was merged' do + before do + subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds) + end + + it 'returns false' do + expect(subject.can_be_reverted?(current_user)).to be_falsey + end + end + + context 'when the revert commit is mentioned in a note long before the MR was merged' do + before do + subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes) + end + + it 'returns true' do + expect(subject.can_be_reverted?(current_user)).to be_truthy + end + end + end + end + end + describe '#participants' do let(:project) { create(:project, :public) } @@ -1195,6 +1339,10 @@ describe MergeRequest do it 'returns false' do expect(subject.mergeable_state?).to be_falsey end + + it 'returns true when skipping discussions check' do + expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true) + end end end end @@ -1395,7 +1543,7 @@ describe MergeRequest do expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1) end - it "executs diff cache service" do + it "executes diff cache service" do expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) subject.reload_diff @@ -1688,7 +1836,7 @@ describe MergeRequest do let(:mr_sha) { merge_request.diff_head_sha } before do - project.team << [developer, :developer] + project.add_developer(developer) end context 'when autocomplete_precheck is set to true' do @@ -1884,4 +2032,56 @@ describe MergeRequest do end end end + + describe '#should_be_rebased?' do + let(:project) { create(:project, :repository) } + + it 'returns false for the same source and target branches' do + merge_request = create(:merge_request, source_project: project, target_project: project) + + expect(merge_request.should_be_rebased?).to be_falsey + end + end + + describe '#rebase_in_progress?' do + shared_examples 'checking whether a rebase is in progress' do + let(:repo_path) { subject.source_project.repository.path } + let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } + + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + end + + it 'returns true when there is a current rebase directory' do + expect(subject.rebase_in_progress?).to be_truthy + end + + it 'returns false when there is no rebase directory' do + FileUtils.rm_rf(rebase_path) + + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the rebase directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, rebase_path) + + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.rebase_in_progress?).to be_falsey + end + end + + context 'when Gitaly rebase_in_progress is enabled' do + it_behaves_like 'checking whether a rebase is in progress' + end + + context 'when Gitaly rebase_in_progress is enabled', :disable_gitaly do + it_behaves_like 'checking whether a rebase is in progress' + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index b7c6286fd83..824cca66fb4 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -203,8 +203,8 @@ describe Namespace do context 'with subgroups' do let(:parent) { create(:group, name: 'parent', path: 'parent') } let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } - let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) } - let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) } + let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child, skip_disk_validation: true) } + let(:uploads_dir) { FileUploader.root } let(:pages_dir) { File.join(TestEnv.pages_path) } before do @@ -240,6 +240,24 @@ describe Namespace do end end end + + it 'updates project full path in .git/config for each project inside namespace' do + parent = create(:group, name: 'mygroup', path: 'mygroup') + subgroup = create(:group, name: 'mysubgroup', path: 'mysubgroup', parent: parent) + project_in_parent_group = create(:project, :repository, namespace: parent, name: 'foo1') + hashed_project_in_subgroup = create(:project, :repository, :hashed, namespace: subgroup, name: 'foo2') + legacy_project_in_subgroup = create(:project, :repository, namespace: subgroup, name: 'foo3') + + parent.update(path: 'mygroup_new') + + expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" + expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" + expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + end + + def project_rugged(project) + project.repository.rugged + end end describe '#rm_dir', 'callback' do @@ -392,17 +410,6 @@ describe Namespace do end end - describe '#soft_delete_without_removing_associations' do - let(:project1) { create(:project_empty_repo, namespace: namespace) } - - it 'updates the deleted_at timestamp but preserves projects' do - namespace.soft_delete_without_removing_associations - - expect(Project.all).to include(project1) - expect(namespace.deleted_at).not_to be_nil - end - end - describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do expect(namespace.user_ids_for_project_authorizations) @@ -589,4 +596,24 @@ describe Namespace do end end end + + describe '#remove_exports' do + let(:legacy_project) { create(:project, :with_export, namespace: namespace) } + let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) } + let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') } + let(:legacy_export) { legacy_project.export_project_path } + let(:hashed_export) { hashed_project.export_project_path } + + it 'removes exports for legacy and hashed projects' do + allow(Gitlab::ImportExport).to receive(:storage_path) { export_path } + + expect(File.exist?(legacy_export)).to be_truthy + expect(File.exist?(hashed_export)).to be_truthy + + namespace.remove_exports! + + expect(File.exist?(legacy_export)).to be_falsy + expect(File.exist?(hashed_export)).to be_falsy + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index cefbf60b28c..c853f707e6d 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -8,7 +8,7 @@ describe Note do it { is_expected.to belong_to(:noteable).touch(false) } it { is_expected.to belong_to(:author).class_name('User') } - it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:todos) } end describe 'modules' do @@ -17,8 +17,6 @@ describe Note do it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Mentionable) } it { is_expected.to include_module(Awardable) } - - it { is_expected.to include_module(Gitlab::CurrentSettings) } end describe 'validation' do @@ -195,7 +193,7 @@ describe Note do describe "cross_reference_not_visible_for?" do let(:private_user) { create(:user) } - let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.team << [private_user, :master] } } + let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } } let(:private_issue) { create(:issue, project: private_project) } let(:ext_proj) { create(:project, :public) } diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 7d835511dfb..9d12f96c642 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -68,7 +68,7 @@ describe PagesDomain do subject { domain.url } context 'without the certificate' do - let(:domain) { build(:pages_domain) } + let(:domain) { build(:pages_domain, certificate: '') } it { is_expected.to eq('http://my.domain.com') } end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index de3e86b627f..63c6fbda3f2 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -37,7 +37,7 @@ describe ProjectFeature do end it "returns true when user is a team member" do - project.team << [user, :developer] + project.add_developer(user) features.each do |feature| project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 41e2ab20d69..1fccf92627a 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -30,7 +30,7 @@ describe ProjectGroupLink do end end - describe "destroying a record", :truncate do + describe "destroying a record", :delete do it "refreshes group users' authorized projects" do project = create(:project, :private) group = create(:group) diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index c9b3c6cf602..748c366efca 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -3,6 +3,29 @@ require 'spec_helper' describe JiraService do include Gitlab::Routing + describe '#options' do + let(:service) do + described_class.new( + project: build_stubbed(:project), + active: true, + username: 'username', + password: 'test', + jira_issue_transition_id: 24, + url: 'http://jira.test.com/path/' + ) + end + + it 'sets the URL properly' do + # jira-ruby gem parses the URI and handles trailing slashes + # fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59 + expect(service.options[:site]).to eq('http://jira.test.com/') + end + + it 'leaves out trailing slashes in context' do + expect(service.options[:context_path]).to eq('/path') + end + end + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -182,7 +205,7 @@ describe JiraService do @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( - body: /#{custom_base_url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/ + body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}} ).once end @@ -197,7 +220,7 @@ describe JiraService do @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( - body: /#{Gitlab.config.gitlab.url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/ + body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}} ).once end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index f037ee77a94..6980ba335b8 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -52,12 +52,75 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when service is inactive' do before do + subject.project = project subject.active = false end it { is_expected.not_to validate_presence_of(:api_url) } it { is_expected.not_to validate_presence_of(:token) } end + + context 'with a deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + before do + kubernetes_service.update_attribute(:active, false) + kubernetes_service.properties[:namespace] = "foo" + end + + it 'should not update attributes' do + expect(kubernetes_service.save).to be_falsy + end + + it 'should include an error with a deprecation message' do + kubernetes_service.valid? + expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/) + end + end + + context 'with a non-deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + it 'should update attributes' do + kubernetes_service.properties[:namespace] = 'foo' + expect(kubernetes_service.save).to be_truthy + end + end + + context 'with an active and deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + before do + kubernetes_service.active = false + kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.save + end + + it 'should deactive the service' do + expect(kubernetes_service.active?).to be_falsy + end + + it 'should not include a deprecation message as error' do + expect(kubernetes_service.errors.messages.count).to eq(0) + end + + it 'should update attributes' do + expect(kubernetes_service.properties[:namespace]).to eq("foo") + end + end + + context 'with a template service' do + let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) } + + before do + kubernetes_service.properties[:namespace] = 'foo' + end + + it 'should update attributes' do + expect(kubernetes_service.save).to be_truthy + expect(kubernetes_service.properties[:namespace]).to eq('foo') + end + end end describe '#initialize_properties' do @@ -318,4 +381,42 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do it { is_expected.to eq(pods: []) } end end + + describe "#deprecated?" do + let(:kubernetes_service) { create(:kubernetes_service) } + + context 'with an active kubernetes service' do + it 'should return false' do + expect(kubernetes_service.deprecated?).to be_falsy + end + end + + context 'with a inactive kubernetes service' do + it 'should return true' do + kubernetes_service.update_attribute(:active, false) + expect(kubernetes_service.deprecated?).to be_truthy + end + end + end + + describe "#deprecation_message" do + let(:kubernetes_service) { create(:kubernetes_service) } + + it 'should indicate the service is deprecated' do + expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/) + end + + context 'if the services is active' do + it 'should return a message' do + expect(kubernetes_service.deprecation_message).to match(/Your cluster information on this page is still editable/) + end + end + + context 'if the service is not active' do + it 'should return a message' do + kubernetes_service.update_attribute(:active, false) + expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/) + end + end + end end diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index 6a5d0decfec..733086e258f 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -92,6 +92,10 @@ describe MicrosoftTeamsService do service.hook_data(merge_request, 'open') end + before do + project.add_developer(user) + end + it "calls Microsoft Teams API" do chat_service.execute(merge_sample_data) diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index be07ca2d945..75ae2207910 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -37,7 +37,7 @@ describe PipelinesEmailService, :mailer do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) end it 'builds test data' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cbeac2f05d3..da940571bc1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -93,7 +93,7 @@ describe Project do let(:developer) { create(:user) } before do project.request_access(requester) - project.team << [developer, :developer] + project.add_developer(developer) end it_behaves_like 'members and requesters associations' do @@ -117,7 +117,6 @@ describe Project do it { is_expected.to include_module(Gitlab::ConfigHelper) } it { is_expected.to include_module(Gitlab::ShellAdapter) } it { is_expected.to include_module(Gitlab::VisibilityLevel) } - it { is_expected.to include_module(Gitlab::CurrentSettings) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } end @@ -418,14 +417,21 @@ describe Project do end describe '#merge_method' do - it 'returns "ff" merge_method when ff is enabled' do - project = build(:project, merge_requests_ff_only_enabled: true) - expect(project.merge_method).to be :ff + using RSpec::Parameterized::TableSyntax + + where(:ff, :rebase, :method) do + true | true | :ff + true | false | :ff + false | true | :rebase_merge + false | false | :merge end - it 'returns "merge" merge_method when ff is disabled' do - project = build(:project, merge_requests_ff_only_enabled: false) - expect(project.merge_method).to be :merge + with_them do + let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) } + + subject { project.merge_method } + + it { is_expected.to eq(method) } end end @@ -520,7 +526,7 @@ describe Project do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'with default issues tracker' do @@ -1435,35 +1441,35 @@ describe Project do let(:user) { create(:user) } it 'returns false when default_branch_protection is in full protection and user is developer' do - project.team << [user, :developer] + project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) expect(project.user_can_push_to_empty_repo?(user)).to be_falsey end it 'returns false when default_branch_protection only lets devs merge and user is dev' do - project.team << [user, :developer] + project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) expect(project.user_can_push_to_empty_repo?(user)).to be_falsey end it 'returns true when default_branch_protection lets devs push and user is developer' do - project.team << [user, :developer] + project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end it 'returns true when default_branch_protection is unprotected and user is developer' do - project.team << [user, :developer] + project.add_developer(user) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end it 'returns true when user is master' do - project.team << [user, :master] + project.add_master(user) expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end @@ -1864,9 +1870,8 @@ describe Project do end it 'creates the new reference with rugged' do - expect(project.repository.rugged.references).to receive(:create).with('HEAD', - "refs/heads/#{project.default_branch}", - force: true) + expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", shell: false) + project.change_head(project.default_branch) end @@ -1945,6 +1950,10 @@ describe Project do expect(second_fork.fork_source).to eq(project) end + + it 'returns nil if it is the root of the fork network' do + expect(project.fork_source).to be_nil + end end describe '#lfs_storage_project' do @@ -2494,6 +2503,37 @@ describe Project do end end + describe '#remove_exports' do + let(:project) { create(:project, :with_export) } + + it 'removes the exports directory for the project' do + expect(File.exist?(project.export_path)).to be_truthy + + allow(FileUtils).to receive(:rm_rf).and_call_original + expect(FileUtils).to receive(:rm_rf).with(project.export_path).and_call_original + project.remove_exports + + expect(File.exist?(project.export_path)).to be_falsy + end + + it 'is a no-op when there is no namespace' do + export_path = project.export_path + project.update_column(:namespace_id, nil) + + expect(FileUtils).not_to receive(:rm_rf).with(export_path) + + project.remove_exports + + expect(File.exist?(export_path)).to be_truthy + end + + it 'is run when the project is destroyed' do + expect(project).to receive(:remove_exports).and_call_original + + project.destroy + end + end + describe '#forks_count' do it 'returns the number of forks' do project = build(:project) @@ -2626,6 +2666,14 @@ describe Project do project.rename_repo end end + + it 'updates project full path in .git/config' do + allow(project_storage).to receive(:rename_repo).and_return(true) + + project.rename_repo + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) + end end describe '#pages_path' do @@ -2668,14 +2716,12 @@ describe Project do end context 'hashed storage' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, skip_disk_validation: true) } let(:gitlab_shell) { Gitlab::Shell.new } - let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } + let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) } before do stub_application_setting(hashed_storage_enabled: true) - allow(Digest::SHA2).to receive(:hexdigest) { hash } - allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) end describe '#legacy_storage?' do @@ -2698,13 +2744,13 @@ describe Project do describe '#base_dir' do it 'returns base_dir based on hash of project id' do - expect(project.base_dir).to eq('@hashed/6b/86') + expect(project.base_dir).to eq("@hashed/#{hash[0..1]}/#{hash[2..3]}") end end describe '#disk_path' do it 'returns disk_path based on hash of project id' do - hashed_path = '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' + hashed_path = "@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}" expect(project.disk_path).to eq(hashed_path) end @@ -2712,7 +2758,9 @@ describe Project do describe '#ensure_storage_path_exists' do it 'delegates to gitlab_shell to ensure namespace is created' do - expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, '@hashed/6b/86') + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + + expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, "@hashed/#{hash[0..1]}/#{hash[2..3]}") project.ensure_storage_path_exists end @@ -2772,7 +2820,7 @@ describe Project do end context 'when not rolled out' do - let(:project) { create(:project, :repository, storage_version: 1) } + let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) } it 'moves pages folder to new location' do expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) @@ -2781,6 +2829,12 @@ describe Project do end end end + + it 'updates project full path in .git/config' do + project.rename_repo + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) + end end describe '#pages_path' do @@ -3058,9 +3112,51 @@ describe Project do expect(project).to receive(:import_finish) expect(project).to receive(:update_project_counter_caches) expect(project).to receive(:remove_import_jid) + expect(project).to receive(:after_create_default_branch) project.after_import end + + context 'branch protection' do + let(:project) { create(:project, :repository) } + + it 'does not protect when branch protection is disabled' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + project.after_import + + expect(project.protected_branches).to be_empty + end + + it "gives developer access to push when branch protection is set to 'developers can push'" do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) + end + + it "gives developer access to merge when branch protection is set to 'developers can merge'" do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) + end + + it 'protects default branch' do + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + end + end end describe '#update_project_counter_caches' do @@ -3123,22 +3219,61 @@ describe Project do end end - describe '#deployment_platform' do - subject { project.deployment_platform } + describe '#write_repository_config' do + set(:project) { create(:project, :repository) } - let(:project) { create(:project) } + it 'writes full path in .git/config when key is missing' do + project.write_repository_config - context 'when user configured kubernetes from Integration > Kubernetes' do - let!(:kubernetes_service) { create(:kubernetes_service, project: project) } + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + end - it { is_expected.to eq(kubernetes_service) } + it 'updates full path in .git/config when key is present' do + project.write_repository_config(gl_full_path: 'old/path') + + expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + end + + it 'does not raise an error with an empty repository' do + project = create(:project_empty_repo) + + expect { project.write_repository_config }.not_to raise_error + end + end + + describe '#execute_hooks' do + it 'executes the projects hooks with the specified scope' do + hook1 = create(:project_hook, merge_requests_events: true, tag_push_events: false) + hook2 = create(:project_hook, merge_requests_events: false, tag_push_events: true) + project = create(:project, hooks: [hook1, hook2]) + + expect_any_instance_of(ProjectHook).to receive(:async_execute).once + + project.execute_hooks({}, :tag_push_hooks) end - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - let(:platform_kubernetes) { cluster.platform_kubernetes } + it 'executes the system hooks with the specified scope' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with({ data: 'data' }, :merge_request_hooks) - it { is_expected.to eq(platform_kubernetes) } + project = build(:project) + project.execute_hooks({ data: 'data' }, :merge_request_hooks) + end + + it 'executes the system hooks when inside a transaction' do + allow_any_instance_of(WebHookService).to receive(:execute) + + create(:system_hook, merge_requests_events: true) + + project = build(:project) + + # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1, + # but since the entire spec run takes place in a transaction, we never + # actually get to the `after_commit` hook that queues these jobs. + expect do + project.transaction do + project.execute_hooks({ data: 'data' }, :merge_request_hooks) + end + end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError end end end diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index e78ed1df821..5cff2af4aca 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -146,6 +146,12 @@ describe ProjectStatistics do expect(statistics.build_artifacts_size).to be(106365) end + + it 'calculates related build artifacts by project' do + expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 } + + statistics.update_build_artifacts_size + end end context 'when legacy artifacts are used' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 314824b32da..e07c522800a 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -291,8 +291,8 @@ describe ProjectTeam do group.add_master(master) group.add_developer(developer) - members_project.team << [developer, :developer] - members_project.team << [master, :master] + members_project.add_developer(developer) + members_project.add_master(master) create(:project_group_link, project: shared_project, group: group) end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 929086305ba..1e7671476f1 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -127,7 +127,7 @@ describe ProjectWiki do end after do - destroy_page(subject.pages.first.page) + subject.pages.each { |page| destroy_page(page.page) } end it "returns the latest version of the page if it exists" do @@ -148,6 +148,17 @@ describe ProjectWiki do page = subject.find_page("index page") expect(page).to be_a WikiPage end + + context 'pages with multibyte-character title' do + before do + create_page("autre pagé", "C'est un génial Gollum Wiki") + end + + it "can find a page by slug" do + page = subject.find_page("autre pagé") + expect(page.title).to eq("autre pagé") + end + end end context 'when Gitaly wiki_find_page is enabled' do diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb index ad3c3a406d9..bfe7a30b96a 100644 --- a/spec/models/push_event_spec.rb +++ b/spec/models/push_event_spec.rb @@ -63,12 +63,14 @@ describe PushEvent do let(:event2) { create(:push_event, project: project) } let(:event3) { create(:push_event, project: project) } let(:event4) { create(:push_event, project: project) } + let(:event5) { create(:push_event, project: project) } before do create(:push_event_payload, event: event1, ref: 'foo', action: :created) create(:push_event_payload, event: event2, ref: 'bar', action: :created) - create(:push_event_payload, event: event3, ref: 'baz', action: :removed) - create(:push_event_payload, event: event4, ref: 'baz', ref_type: :tag) + create(:push_event_payload, event: event3, ref: 'qux', action: :created) + create(:push_event_payload, event: event4, ref: 'baz', action: :removed) + create(:push_event_payload, event: event5, ref: 'baz', ref_type: :tag) project.repository.create_branch('bar', 'master') @@ -78,6 +80,16 @@ describe PushEvent do target_project: project, source_branch: 'bar' ) + + project.repository.create_branch('qux', 'master') + + create( + :merge_request, + :closed, + source_project: project, + target_project: project, + source_branch: 'qux' + ) end let(:relation) { described_class.without_existing_merge_requests } @@ -86,16 +98,20 @@ describe PushEvent do expect(relation).to include(event1) end - it 'does not include events that have a corresponding merge request' do + it 'does not include events that have a corresponding open merge request' do expect(relation).not_to include(event2) end + it 'includes events that has corresponding closed/merged merge requests' do + expect(relation).to include(event3) + end + it 'does not include events for removed refs' do - expect(relation).not_to include(event3) + expect(relation).not_to include(event4) end it 'does not include events for pushing to tags' do - expect(relation).not_to include(event4) + expect(relation).not_to include(event5) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9a68ae086ea..02a5ee54262 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -36,26 +36,49 @@ describe Repository do end describe '#branch_names_contains' do - subject { repository.branch_names_contains(sample_commit.id) } + shared_examples '#branch_names_contains' do + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - it { is_expected.to include('master') } - it { is_expected.not_to include('feature') } - it { is_expected.not_to include('fix') } + subject { repository.branch_names_contains(sample_commit.id) } - describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do - expect_to_raise_storage_error do - broken_repository.branch_names_contains(sample_commit.id) + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } + + describe 'when storage is broken', :broken_storage do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.branch_names_contains(sample_commit.id) + end end end end + + context 'when gitaly is enabled' do + it_behaves_like '#branch_names_contains' + end + + context 'when gitaly is disabled', :skip_gitaly_mock do + it_behaves_like '#branch_names_contains' + end end describe '#tag_names_contains' do - subject { repository.tag_names_contains(sample_commit.id) } + shared_examples '#tag_names_contains' do + subject { repository.tag_names_contains(sample_commit.id) } - it { is_expected.to include('v1.1.0') } - it { is_expected.not_to include('v1.0.0') } + it { is_expected.to include('v1.1.0') } + it { is_expected.not_to include('v1.0.0') } + end + + context 'when gitaly is enabled' do + it_behaves_like '#tag_names_contains' + end + + context 'when gitaly is enabled', :skip_gitaly_mock do + it_behaves_like '#tag_names_contains' + end end describe 'tags_sorted_by' do @@ -222,20 +245,20 @@ describe Repository do it 'sets follow when path is a single path' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice - repository.commits('master', path: 'README.md') - repository.commits('master', path: ['README.md']) + repository.commits('master', limit: 1, path: 'README.md') + repository.commits('master', limit: 1, path: ['README.md']) end it 'does not set follow when path is multiple paths' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original - repository.commits('master', path: ['README.md', 'CHANGELOG']) + repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG']) end it 'does not set follow when there are no paths' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original - repository.commits('master') + repository.commits('master', limit: 1) end end @@ -358,28 +381,44 @@ describe Repository do end describe '#can_be_merged?' do - context 'mergeable branches' do - subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } + shared_examples 'can be merged' do + context 'mergeable branches' do + subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } - it { is_expected.to be_truthy } - end + it { is_expected.to be_truthy } + end - context 'non-mergeable branches' do - subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') } + context 'non-mergeable branches without conflict sides missing' do + subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') } - it { is_expected.to be_falsey } - end + it { is_expected.to be_falsey } + end - context 'non merged branch' do - subject { repository.merged_to_root_ref?('fix') } + context 'non-mergeable branches with conflict sides missing' do + subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') } - it { is_expected.to be_falsey } + it { is_expected.to be_falsey } + end + + context 'non merged branch' do + subject { repository.merged_to_root_ref?('fix') } + + it { is_expected.to be_falsey } + end + + context 'non existent branch' do + subject { repository.merged_to_root_ref?('non_existent_branch') } + + it { is_expected.to be_nil } + end end - context 'non existent branch' do - subject { repository.merged_to_root_ref?('non_existent_branch') } + context 'when Gitaly can_be_merged feature is enabled' do + it_behaves_like 'can be merged' + end - it { is_expected.to be_nil } + context 'when Gitaly can_be_merged feature is disabled', :disable_gitaly do + it_behaves_like 'can be merged' end end @@ -412,12 +451,34 @@ describe Repository do end end + describe '#create_hooks' do + let(:hook_path) { File.join(repository.path_to_repo, 'hooks') } + + it 'symlinks the global hooks directory' do + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + + it 'replaces existing symlink with the right directory' do + FileUtils.mkdir_p(hook_path) + + expect(File.symlink?(hook_path)).to be false + + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + end + describe "#create_dir" do it "commits a change that creates a new directory" do expect do repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) newdir = repository.tree('master', 'newdir') expect(newdir.path).to eq('newdir') @@ -431,7 +492,7 @@ describe Repository do repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', start_branch_name: 'master', start_project: forked_project) - end.to change { repository.commits('master').count }.by(0) + end.to change { repository.count_commits(ref: 'master') }.by(0) expect(repository.branch_exists?('patch')).to be_truthy expect(forked_project.repository.branch_exists?('patch')).to be_falsy @@ -448,7 +509,7 @@ describe Repository do message: 'Add newdir', branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -464,7 +525,7 @@ describe Repository do repository.create_file(user, 'NEWCHANGELOG', 'Changelog!', message: 'Create changelog', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) blob = repository.blob_at('master', 'NEWCHANGELOG') @@ -476,7 +537,7 @@ describe Repository do repository.create_file(user, 'new_dir/new_file.txt', 'File!', message: 'Create new_file with new_dir', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) expect(repository.tree('master', 'new_dir').path).to eq('new_dir') expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!') @@ -500,7 +561,7 @@ describe Repository do branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -516,7 +577,7 @@ describe Repository do repository.update_file(user, 'CHANGELOG', 'Changelog!', message: 'Update changelog', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) blob = repository.blob_at('master', 'CHANGELOG') @@ -529,7 +590,7 @@ describe Repository do branch_name: 'master', previous_path: 'LICENSE', message: 'Changes filename') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) files = repository.ls_files('master') @@ -546,7 +607,7 @@ describe Repository do message: 'Update README', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -561,7 +622,7 @@ describe Repository do expect do repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) expect(repository.blob_at('master', 'README')).to be_nil end @@ -572,7 +633,7 @@ describe Repository do repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -582,38 +643,6 @@ describe Repository do end end - describe '#get_committer_and_author' do - it 'returns the committer and author data' do - options = repository.get_committer_and_author(user) - expect(options[:committer][:email]).to eq(user.email) - expect(options[:author][:email]).to eq(user.email) - end - - context 'when the email/name are given' do - it 'returns an object containing the email/name' do - options = repository.get_committer_and_author(user, email: author_email, name: author_name) - expect(options[:author][:email]).to eq(author_email) - expect(options[:author][:name]).to eq(author_name) - end - end - - context 'when the email is given but the name is not' do - it 'returns the committer as the author' do - options = repository.get_committer_and_author(user, email: author_email) - expect(options[:author][:email]).to eq(user.email) - expect(options[:author][:name]).to eq(user.name) - end - end - - context 'when the name is given but the email is not' do - it 'returns nil' do - options = repository.get_committer_and_author(user, name: author_name) - expect(options[:author][:email]).to eq(user.email) - expect(options[:author][:name]).to eq(user.name) - end - end - end - describe "search_files_by_content" do let(:results) { repository.search_files_by_content('feature', 'master') } subject { results } @@ -657,7 +686,7 @@ describe Repository do subject { results.first } it { is_expected.to be_an String } - it { expect(subject.lines[2]).to eq("master:CHANGELOG:190: - Feature: Replace teams with group membership\n") } + it { expect(subject.lines[2]).to eq("master:CHANGELOG\x00190\x00 - Feature: Replace teams with group membership\n") } end end @@ -668,6 +697,18 @@ describe Repository do expect(results.first).to eq('files/html/500.html') end + it 'ignores leading slashes' do + results = repository.search_files_by_name('/files', 'master') + + expect(results.first).to eq('files/html/500.html') + end + + it 'properly handles when query is only slashes' do + results = repository.search_files_by_name('//', 'master') + + expect(results).to match_array([]) + end + it 'properly handles when query is not present' do results = repository.search_files_by_name('', 'master') @@ -754,8 +795,7 @@ describe Repository do user, 'LICENSE', 'Copyright!', message: 'Add LICENSE', branch_name: 'master') - allow(repository).to receive(:file_on_head) - .and_raise(Rugged::ReferenceError) + allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository) expect(repository.license_blob).to be_nil end @@ -867,7 +907,7 @@ describe Repository do end it 'returns nil for empty repository' do - allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError) + allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository) expect(repository.gitlab_ci_yml).to be_nil end end @@ -942,19 +982,19 @@ describe Repository do end describe '#find_branch' do - it 'loads a branch with a fresh repo' do - expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original + context 'fresh_repo is true' do + it 'delegates the call to raw_repository' do + expect(repository.raw_repository).to receive(:find_branch).with('master', true) - 2.times do - expect(repository.find_branch('feature')).not_to be_nil + repository.find_branch('master', fresh_repo: true) end end - it 'loads a branch with a cached repo' do - expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original + context 'fresh_repo is false' do + it 'delegates the call to raw_repository' do + expect(repository.raw_repository).to receive(:find_branch).with('master', false) - 2.times do - expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil + repository.find_branch('master', fresh_repo: false) end end end @@ -1112,16 +1152,16 @@ describe Repository do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) end - it 'expires branch cache' do - expect(repository).not_to receive(:expire_exists_cache) - expect(repository).not_to receive(:expire_root_ref_cache) - expect(repository).not_to receive(:expire_emptiness_caches) - expect(repository).to receive(:expire_branches_cache) - - repository.with_branch(user, 'new-feature') do + subject do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('new-feature') do new_rev end end + + it 'returns branch_created as true' do + expect(subject).not_to be_repo_created + expect(subject).to be_branch_created + end end context 'when repository is empty' do @@ -1919,8 +1959,7 @@ describe Repository do describe '#avatar' do it 'returns nil if repo does not exist' do - expect(repository).to receive(:file_on_head) - .and_raise(Rugged::ReferenceError) + allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository) expect(repository.avatar).to eq(nil) end @@ -2215,6 +2254,15 @@ describe Repository do end end + describe '#diverging_commit_counts' do + it 'returns the commit counts behind and ahead of default branch' do + result = repository.diverging_commit_counts( + repository.find_branch('fix')) + + expect(result).to eq(behind: 29, ahead: 2) + end + end + describe '#cache_method_output', :use_clean_rails_memory_store_caching do let(:fallback) { 10 } @@ -2329,7 +2377,7 @@ describe Repository do let(:commit) { repository.commit } let(:ancestor) { commit.parents.first } - context 'with Gitaly enabled' do + shared_examples '#ancestor?' do it 'it is an ancestor' do expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true) end @@ -2343,27 +2391,19 @@ describe Repository do expect(repository.ancestor?(ancestor.id, nil)).to eq(false) expect(repository.ancestor?(nil, nil)).to eq(false) end - end - - context 'with Gitaly disabled' do - before do - allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false) - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false) - end - it 'it is an ancestor' do - expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true) + it 'returns false for invalid commit IDs' do + expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false) + expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false) end + end - it 'it is not an ancestor' do - expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false) - end + context 'with Gitaly enabled' do + it_behaves_like('#ancestor?') + end - it 'returns false on nil-values' do - expect(repository.ancestor?(nil, commit.id)).to eq(false) - expect(repository.ancestor?(ancestor.id, nil)).to eq(false) - expect(repository.ancestor?(nil, nil)).to eq(false) - end + context 'with Gitaly disabled', :skip_gitaly_mock do + it_behaves_like('#ancestor?') end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index ddad6862a63..88f54fd10e5 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -16,9 +16,76 @@ describe Route do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path).case_insensitive } + + describe '#ensure_permanent_paths' do + context 'when the route is not yet persisted' do + let(:new_route) { described_class.new(path: 'foo', source: build(:group)) } + + context 'when permanent conflicting redirects exist' do + it 'is invalid' do + redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz') + redirect.save!(validate: false) + + expect(new_route.valid?).to be_falsey + expect(new_route.errors.first[1]).to eq('foo has been taken before. Please use another one') + end + end + + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(new_route.valid?).to be_truthy + end + end + end + + context 'when path has changed' do + before do + route.path = 'foo' + end + + context 'when permanent conflicting redirects exist' do + it 'is invalid' do + redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz') + redirect.save!(validate: false) + + expect(route.valid?).to be_falsey + expect(route.errors.first[1]).to eq('foo has been taken before. Please use another one') + end + end + + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(route.valid?).to be_truthy + end + end + end + + context 'when path has not changed' do + context 'when permanent conflicting redirects exist' do + it 'is valid' do + redirect = build(:redirect_route, :permanent, path: 'git_lab/foo/bar') + redirect.save!(validate: false) + + expect(route.valid?).to be_truthy + end + end + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(route.valid?).to be_truthy + end + end + end + end end describe 'callbacks' do + context 'before validation' do + it 'calls #delete_conflicting_orphaned_routes' do + expect(route).to receive(:delete_conflicting_orphaned_routes) + route.valid? + end + end + context 'after update' do it 'calls #create_redirect_for_old_path' do expect(route).to receive(:create_redirect_for_old_path) @@ -318,4 +385,58 @@ describe Route do end end end + + describe '#delete_conflicting_orphaned_routes' do + context 'when there is a conflicting route' do + let!(:conflicting_group) { create(:group, path: 'foo') } + + before do + route.path = conflicting_group.route.path + end + + context 'when the route is orphaned' do + let!(:offending_route) { conflicting_group.route } + + before do + Group.delete(conflicting_group) # Orphan the route + end + + it 'deletes the orphaned route' do + expect do + route.valid? + end.to change { described_class.count }.from(2).to(1) + end + + it 'passes validation, as usual' do + expect(route.valid?).to be_truthy + end + end + + context 'when the route is not orphaned' do + it 'does not delete the conflicting route' do + expect do + route.valid? + end.not_to change { described_class.count } + end + + it 'fails validation, as usual' do + expect(route.valid?).to be_falsey + end + end + end + + context 'when there are no conflicting routes' do + it 'does not delete any routes' do + route + + expect do + route.valid? + end.not_to change { described_class.count } + end + + it 'passes validation, as usual' do + expect(route.valid?).to be_truthy + end + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0f2f906c667..79f25dc4360 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -254,4 +254,64 @@ describe Service do end end end + + describe "#deprecated?" do + let(:project) { create(:project, :repository) } + + it 'should return false by default' do + service = create(:service, project: project) + expect(service.deprecated?).to be_falsy + end + end + + describe "#deprecation_message" do + let(:project) { create(:project, :repository) } + + it 'should be empty by default' do + service = create(:service, project: project) + expect(service.deprecation_message).to be_nil + end + end + + describe '.find_by_template' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + it 'returns service template' do + expect(KubernetesService.find_by_template).to eq(kubernetes_service) + end + end + + describe '#api_field_names' do + let(:fake_service) do + Class.new(Service) do + def fields + [ + { name: 'token' }, + { name: 'api_token' }, + { name: 'key' }, + { name: 'api_key' }, + { name: 'password' }, + { name: 'password_field' }, + { name: 'safe_field' } + ] + end + end + end + + let(:service) do + fake_service.new(properties: [ + { token: 'token-value' }, + { api_token: 'api_token-value' }, + { key: 'key-value' }, + { api_key: 'api_key-value' }, + { password: 'password-value' }, + { password_field: 'password_field-value' }, + { safe_field: 'safe_field-value' } + ]) + end + + it 'filters out sensitive fields' do + expect(service.api_field_names).to eq(['safe_field']) + end + end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 3e8f3848eca..bd498269798 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -20,6 +20,7 @@ describe Todo do it { is_expected.to validate_presence_of(:action) } it { is_expected.to validate_presence_of(:target_type) } it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:author) } context 'for commits' do subject { described_class.new(target_type: 'Commit') } diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 345382ea8c7..0dcaa026332 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -45,51 +45,6 @@ describe Upload do end end - describe '.remove_path' do - it 'removes all records at the given path' do - described_class.create!( - size: File.size(__FILE__), - path: __FILE__, - model: build_stubbed(:user), - uploader: 'AvatarUploader' - ) - - expect { described_class.remove_path(__FILE__) } - .to change { described_class.count }.from(1).to(0) - end - end - - describe '.record' do - let(:fake_uploader) do - double( - file: double(size: 12_345), - relative_path: 'foo/bar.jpg', - model: build_stubbed(:user), - class: 'AvatarUploader' - ) - end - - it 'removes existing paths before creation' do - expect(described_class).to receive(:remove_path) - .with(fake_uploader.relative_path) - - described_class.record(fake_uploader) - end - - it 'creates a new record and assigns size, path, model, and uploader' do - upload = described_class.record(fake_uploader) - - aggregate_failures do - expect(upload).to be_persisted - expect(upload.size).to eq fake_uploader.file.size - expect(upload.path).to eq fake_uploader.relative_path - expect(upload.model_id).to eq fake_uploader.model.id - expect(upload.model_type).to eq fake_uploader.model.class.to_s - expect(upload.uploader).to eq fake_uploader.class - end - end - end - describe '#absolute_path' do it 'returns the path directly when already absolute' do path = '/path/to/namespace/project/secret/file.jpg' @@ -111,27 +66,27 @@ describe Upload do end end - describe '#calculate_checksum' do - it 'calculates the SHA256 sum' do - upload = described_class.new( - path: __FILE__, - size: described_class::CHECKSUM_THRESHOLD - 1.megabyte - ) + describe '#calculate_checksum!' do + let(:upload) do + described_class.new(path: __FILE__, + size: described_class::CHECKSUM_THRESHOLD - 1.megabyte) + end + + it 'sets `checksum` to SHA256 sum of the file' do expected = Digest::SHA256.file(__FILE__).hexdigest - expect { upload.calculate_checksum } + expect { upload.calculate_checksum! } .to change { upload.checksum }.from(nil).to(expected) end - it 'returns nil for a non-existant file' do - upload = described_class.new( - path: __FILE__, - size: described_class::CHECKSUM_THRESHOLD - 1.megabyte - ) - + it 'sets `checksum` to nil for a non-existant file' do expect(upload).to receive(:exist?).and_return(false) - expect(upload.calculate_checksum).to be_nil + checksum = Digest::SHA256.file(__FILE__).hexdigest + upload.checksum = checksum + + expect { upload.calculate_checksum! } + .to change { upload.checksum }.from(checksum).to(nil) end end @@ -148,4 +103,10 @@ describe Upload do expect(upload).not_to exist end end + + describe "#uploader_context" do + subject { create(:upload, :issuable_upload, secret: 'secret', filename: 'file.txt') } + + it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) } + end end diff --git a/spec/models/user_callout_spec.rb b/spec/models/user_callout_spec.rb new file mode 100644 index 00000000000..64ba17c81fe --- /dev/null +++ b/spec/models/user_callout_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe UserCallout do + let!(:callout) { create(:user_callout) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to validate_presence_of(:feature_name) } + it { is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id).ignoring_case_sensitivity } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e58e7588df0..af79ea4c283 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,14 +1,12 @@ require 'spec_helper' describe User do - include Gitlab::CurrentSettings include ProjectForksHelper describe 'modules' do subject { described_class } it { is_expected.to include_module(Gitlab::ConfigHelper) } - it { is_expected.to include_module(Gitlab::CurrentSettings) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } it { is_expected.to include_module(TokenAuthenticatable) } @@ -22,7 +20,9 @@ describe User do describe 'associations' do it { is_expected.to have_one(:namespace) } it { is_expected.to have_many(:snippets).dependent(:destroy) } - it { is_expected.to have_many(:project_members).dependent(:destroy) } + it { is_expected.to have_many(:members) } + it { is_expected.to have_many(:project_members) } + it { is_expected.to have_many(:group_members) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } @@ -33,7 +33,7 @@ describe User do it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } - it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:todos) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:triggers).dependent(:destroy) } it { is_expected.to have_many(:builds).dependent(:nullify) } @@ -134,6 +134,16 @@ describe User do end end + it 'has a DB-level NOT NULL constraint on projects_limit' do + user = create(:user) + + expect(user.persisted?).to eq(true) + + expect do + user.update_columns(projects_limit: nil) + end.to raise_error(ActiveRecord::StatementInvalid) + end + it { is_expected.to validate_presence_of(:projects_limit) } it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) } @@ -548,7 +558,7 @@ describe User do stub_config_setting(default_can_create_group: true) expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true) - .and change { user.projects_limit }.to(current_application_settings.default_projects_limit) + .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit) end end @@ -760,7 +770,7 @@ describe User do before do # add user to project - project.team << [user, :master] + project.add_master(user) # create invite to projet create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com') @@ -805,9 +815,16 @@ describe User do expect(user.can_create_group).to be_falsey expect(user.theme_id).to eq(1) end + + it 'does not undo projects_limit setting if it matches old DB default of 10' do + # If the real default project limit is 10 then this test is worthless + expect(Gitlab.config.gitlab.default_projects_limit).not_to eq(10) + user = described_class.new(projects_limit: 10) + expect(user.projects_limit).to eq(10) + end end - context 'when current_application_settings.user_default_external is true' do + context 'when Gitlab::CurrentSettings.user_default_external is true' do before do stub_application_setting(user_default_external: true) end @@ -947,6 +964,14 @@ describe User do expect(described_class.search(user3.username.upcase)).to eq([user3]) end end + + it 'returns no matches for an empty string' do + expect(described_class.search('')).to be_empty + end + + it 'returns no matches for nil' do + expect(described_class.search(nil)).to be_empty + end end describe '.search_with_secondary_emails' do @@ -1001,6 +1026,14 @@ describe User do it 'does not return users with a matching part of secondary email' do expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user]) end + + it 'returns no matches for an empty string' do + expect(search_with_secondary_emails('')).to be_empty + end + + it 'returns no matches for nil' do + expect(search_with_secondary_emails(nil)).to be_empty + end end describe '.find_by_ssh_key_id' do @@ -1448,8 +1481,8 @@ describe User do let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) } before do - project1.team << [subject, :master] - project2.team << [subject, :master] + project1.add_master(subject) + project2.add_master(subject) end it "includes IDs for projects the user has pushed to" do @@ -1534,7 +1567,7 @@ describe User do it { is_expected.to eq([private_group]) } end - describe '#authorized_projects', :truncate do + describe '#authorized_projects', :delete do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do user = create(:user) @@ -1548,7 +1581,7 @@ describe User do user = create(:user) project = create(:project, :private) - project.team << [user, Gitlab::Access::MASTER] + project.add_master(user) expect(user.authorized_projects(Gitlab::Access::REPORTER)) .to contain_exactly(project) @@ -1567,7 +1600,7 @@ describe User do user2 = create(:user) project = create(:project, :private, namespace: user1.namespace) - project.team << [user2, Gitlab::Access::DEVELOPER] + project.add_developer(user2) expect(user2.authorized_projects).to include(project) end @@ -1612,7 +1645,7 @@ describe User do user2 = create(:user) project = create(:project, :private, namespace: user1.namespace) - project.team << [user2, Gitlab::Access::DEVELOPER] + project.add_developer(user2) expect(user2.authorized_projects).to include(project) @@ -1702,7 +1735,7 @@ describe User do shared_examples :member do context 'when the user is a master' do before do - add_user(Gitlab::Access::MASTER) + add_user(:master) end it 'loads' do @@ -1712,7 +1745,7 @@ describe User do context 'when the user is a developer' do before do - add_user(Gitlab::Access::DEVELOPER) + add_user(:developer) end it 'does not load' do @@ -1736,7 +1769,7 @@ describe User do let(:project) { create(:project) } def add_user(access) - project.team << [user, access] + project.add_role(user, access) end it_behaves_like :member @@ -1749,8 +1782,8 @@ describe User do let(:user) { create(:user) } before do - project1.team << [user, :reporter] - project2.team << [user, :guest] + project1.add_reporter(user) + project2.add_guest(user) end it 'returns the projects when using a single project ID' do @@ -1892,8 +1925,8 @@ describe User do let(:user) { create(:user) } before do - project1.team << [user, :reporter] - project2.team << [user, :guest] + project1.add_reporter(user) + project2.add_guest(user) user.project_authorizations.delete_all user.refresh_authorized_projects diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index ea75434e399..4a1d12cd448 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -188,14 +188,37 @@ describe WikiPage do end end - describe "#update" do + describe '#create', :skip_gitaly_mock do + context 'with valid attributes' do + it 'raises an error if a page with the same path already exists' do + create_page('New Page', 'content') + create_page('foo/bar', 'content') + expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + + destroy_page('New Page') + destroy_page('bar', 'foo') + end + + it 'if the title is preceded by a / it is removed' do + create_page('/New Page', 'content') + + expect(wiki.find_page('New Page')).not_to be_nil + + destroy_page('New Page') + end + end + end + + # Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages + describe "#update", :skip_gitaly_mock do before do create_page("Update", "content") @page = wiki.find_page("Update") end after do - destroy_page(@page.title) + destroy_page(@page.title, @page.directory) end context "with valid attributes" do @@ -233,6 +256,95 @@ describe WikiPage do expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) end end + + context 'when renaming a page' do + it 'raises an error if the page already exists' do + create_page('Existing Page', 'content') + + expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' + + destroy_page('Existing Page') + end + + it 'updates the content and rename the file' do + new_title = 'Renamed Page' + new_content = 'updated content' + + expect(@page.update(title: new_title, content: new_content)).to be_truthy + + @page = wiki.find_page(new_title) + + expect(@page).not_to be_nil + expect(@page.content).to eq new_content + end + end + + context 'when moving a page' do + it 'raises an error if the page already exists' do + create_page('foo/Existing Page', 'content') + + expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' + + destroy_page('Existing Page', 'foo') + end + + it 'updates the content and moves the file' do + new_title = 'foo/Other Page' + new_content = 'new_content' + + expect(@page.update(title: new_title, content: new_content)).to be_truthy + + page = wiki.find_page(new_title) + + expect(page).not_to be_nil + expect(page.content).to eq new_content + end + + context 'in subdir' do + before do + create_page('foo/Existing Page', 'content') + @page = wiki.find_page('foo/Existing Page') + end + + it 'moves the page to the root folder if the title is preceded by /' do + expect(@page.slug).to eq 'foo/Existing-Page' + expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq 'Existing-Page' + end + + it 'does nothing if it has the same title' do + original_path = @page.slug + + expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end + end + + context 'in root dir' do + it 'does nothing if the title is preceded by /' do + original_path = @page.slug + + expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end + end + end + + context "with invalid attributes" do + it 'aborts update if title blank' do + expect(@page.update(title: '', content: 'new_content')).to be_falsey + expect(@page.content).to eq 'new_content' + + page = wiki.find_page('Update') + expect(page.content).to eq 'content' + + @page.title = 'Update' + end + end end describe "#destroy" do @@ -386,6 +498,27 @@ describe WikiPage do end end + describe '#formatted_content' do + shared_examples 'fetching page formatted content' do + it 'returns processed content of the page' do + subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" }) + page = wiki.find_page('RDoc') + + expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n") + + destroy_page('RDoc') + end + end + + context 'when Gitaly wiki_page_formatted_data is enabled' do + it_behaves_like 'fetching page formatted content' + end + + context 'when Gitaly wiki_page_formatted_data is disabled', :disable_gitaly do + it_behaves_like 'fetching page formatted content' + end + end + private def remove_temp_repo(path) @@ -400,8 +533,8 @@ describe WikiPage do wiki.wiki.write_page(name, :markdown, content, commit_details) end - def destroy_page(title) - page = wiki.wiki.page(title: title) + def destroy_page(title, dir = '') + page = wiki.wiki.page(title: title, dir: dir) wiki.delete_page(page, "test commit") end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 298a9d16425..41cf2ef7225 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -57,7 +57,7 @@ describe Ci::BuildPolicy do context 'team member is a guest' do before do - project.team << [user, :guest] + project.add_guest(user) end context 'when public builds are enabled' do @@ -77,7 +77,7 @@ describe Ci::BuildPolicy do context 'team member is a reporter' do before do - project.team << [user, :reporter] + project.add_reporter(user) end context 'when public builds are enabled' do diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb index 1b0e9fac355..c0c3eda4911 100644 --- a/spec/policies/ci/pipeline_schedule_policy_spec.rb +++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb @@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do expect(policy).to be_allowed :admin_pipeline_schedule end end + + describe 'rules for non-owner of schedule' do + let(:owner) { create(:user) } + + before do + project.add_master(owner) + project.add_master(user) + pipeline_schedule.update(owner: owner) + end + + it 'includes abilities to take ownership' do + expect(policy).to be_allowed :take_ownership_pipeline_schedule + end + end end end diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb index be40dbb2aa9..14630748c90 100644 --- a/spec/policies/ci/trigger_policy_spec.rb +++ b/spec/policies/ci/trigger_policy_spec.rb @@ -45,7 +45,7 @@ describe Ci::TriggerPolicy do context 'when user is master of the project' do before do - project.team << [user, :master] + project.add_master(user) end it_behaves_like 'allows to admin and manage trigger' @@ -53,7 +53,7 @@ describe Ci::TriggerPolicy do context 'when user is developer of the project' do before do - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like 'disallows to admin and manage trigger' @@ -69,7 +69,7 @@ describe Ci::TriggerPolicy do context 'when user is master of the project' do before do - project.team << [user, :master] + project.add_master(user) end it_behaves_like 'allows to admin and manage trigger' @@ -81,7 +81,7 @@ describe Ci::TriggerPolicy do context 'when user is master of the project' do before do - project.team << [user, :master] + project.add_master(user) end it_behaves_like 'allows to manage trigger' @@ -89,7 +89,7 @@ describe Ci::TriggerPolicy do context 'when user is developer of the project' do before do - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like 'disallows to admin and manage trigger' diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index be4c24c727c..a4af9361ea6 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -19,10 +19,10 @@ describe IssuePolicy do let(:issue_no_assignee) { create(:issue, project: project) } before do - project.team << [guest, :guest] - project.team << [author, :guest] - project.team << [assignee, :guest] - project.team << [reporter, :reporter] + project.add_guest(guest) + project.add_guest(author) + project.add_guest(assignee) + project.add_reporter(reporter) group.add_reporter(reporter_from_group_link) @@ -114,8 +114,8 @@ describe IssuePolicy do let(:issue_no_assignee) { create(:issue, project: project) } before do - project.team << [guest, :guest] - project.team << [reporter, :reporter] + project.add_guest(guest) + project.add_reporter(reporter) group.add_reporter(reporter_from_group_link) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index f2593a1a75c..129344f105f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -92,7 +92,7 @@ describe ProjectPolicy do it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:project, :private) - issue = create(:issue, project: project) + issue = create(:issue, project: project, author: create(:user)) user = issue.author expect(project.team.member?(issue.author)).to be false diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index f0bf46c480a..cdba1b09fc1 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -87,7 +87,7 @@ describe ProjectSnippetPolicy do subject { abilities(external_user, :internal) } before do - project.team << [external_user, :developer] + project.add_developer(external_user) end it do @@ -131,7 +131,7 @@ describe ProjectSnippetPolicy do subject { abilities(regular_user, :private) } before do - project.team << [regular_user, :developer] + project.add_developer(regular_user) end it do @@ -144,7 +144,7 @@ describe ProjectSnippetPolicy do subject { abilities(external_user, :private) } before do - project.team << [external_user, :developer] + project.add_developer(external_user) end it do diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index f325d1776e4..e3b37739e8e 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -116,7 +116,7 @@ describe MergeRequestPresenter do end before do - project.team << [user, :developer] + project.add_developer(user) allow(resource.project).to receive(:default_branch) .and_return(resource.target_branch) @@ -270,7 +270,7 @@ describe MergeRequestPresenter do context 'when can create issue and issues enabled' do it 'returns path' do allow(project).to receive(:issues_enabled?) { true } - project.team << [user, :master] + project.add_master(user) is_expected .to eq("/#{resource.project.full_path}/issues/new?merge_request_to_resolve_discussions_of=#{resource.iid}") @@ -288,7 +288,7 @@ describe MergeRequestPresenter do context 'when issues disabled' do it 'returns nil' do allow(project).to receive(:issues_enabled?) { false } - project.team << [user, :master] + project.add_master(user) is_expected.to be_nil end @@ -307,7 +307,7 @@ describe MergeRequestPresenter do context 'when merge request enabled and has permission' do it 'has remove_wip_path' do allow(project).to receive(:merge_requests_enabled?) { true } - project.team << [user, :master] + project.add_master(user) is_expected .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip") @@ -404,4 +404,67 @@ describe MergeRequestPresenter do .to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>") end end + + describe '#rebase_path' do + before do + allow(resource).to receive(:rebase_in_progress?) { rebase_in_progress } + allow(resource).to receive(:should_be_rebased?) { should_be_rebased } + + allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension) + .to receive(:can_push_to_branch?) + .with(resource.source_branch) + .and_return(can_push_to_branch) + end + + subject do + described_class.new(resource, current_user: user).rebase_path + end + + context 'when can rebase' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + before do + allow(resource).to receive(:source_branch_exists?) { true } + end + + it 'returns path' do + is_expected + .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase") + end + end + + context 'when cannot rebase' do + context 'when rebase in progress' do + let(:rebase_in_progress) { true } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when user cannot merge' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { false } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'should not be rebased' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { false } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + end end diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 35ca3635a9d..24389f28b21 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -8,8 +8,8 @@ describe API::AccessRequests do set(:project) do create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| - project.team << [developer, :developer] - project.team << [master, :master] + project.add_developer(developer) + project.add_master(master) project.request_access(access_requester) end end diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb new file mode 100644 index 00000000000..f56bc932f40 --- /dev/null +++ b/spec/requests/api/applications_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe API::Applications, :api do + include ApiHelpers + + let(:admin_user) { create(:user, admin: true) } + let(:user) { create(:user, admin: false) } + + describe 'POST /applications' do + context 'authenticated and authorized user' do + it 'creates and returns an OAuth application' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.to change { Doorkeeper::Application.count }.by 1 + + application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url') + + expect(response).to have_http_status 201 + expect(json_response).to be_a Hash + expect(json_response['application_id']).to eq application.uid + expect(json_response['secret']).to eq application.secret + expect(json_response['callback_url']).to eq application.redirect_uri + end + + it 'does not allow creating an application with the wrong redirect_uri format' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.') + end + + it 'does not allow creating an application without a name' do + expect do + post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('name is missing') + end + + it 'does not allow creating an application without a redirect_uri' do + expect do + post api('/applications', admin_user), name: 'application_name', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('redirect_uri is missing') + end + + it 'does not allow creating an application without scopes' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('scopes is missing') + end + end + + context 'authorized user without authorization' do + it 'does not create application' do + expect do + post api('/applications', user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 403 + end + end + + context 'non-authenticated user' do + it 'does not create application' do + expect do + post api('/applications'), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 401 + end + end + end +end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index eaf12f71421..5adfb33677f 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -10,7 +10,7 @@ describe API::AwardEmoji do set(:note) { create(:note, project: project, noteable: issue) } before do - project.team << [user, :master] + project.add_master(user) end describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index 546a1697e56..c6c10025f7f 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -6,18 +6,18 @@ describe API::Boards do set(:non_member) { create(:user) } set(:guest) { create(:user) } set(:admin) { create(:user, :admin) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) + create(:label, title: 'Development', color: '#FFAABB', project: board_parent) end set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) + create(:label, title: 'Testing', color: '#FFAACC', project: board_parent) end set(:ux_label) do - create(:label, title: 'UX', color: '#FF0000', project: project) + create(:label, title: 'UX', color: '#FF0000', project: board_parent) end set(:dev_list) do @@ -28,180 +28,25 @@ describe API::Boards do create(:list, label: test_label, position: 2) end - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end - - before do - project.team << [user, :reporter] - project.team << [guest, :guest] - end + set(:milestone) { create(:milestone, project: board_parent) } + set(:board_label) { create(:label, project: board_parent) } + set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) } - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } + it_behaves_like 'group and project boards', "/projects/:id/boards" - context "when unauthenticated" do - it "returns authentication error" do - get api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get api(base_url, 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.length).to eq(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get api(base_url, 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.length).to eq(2) - expect(json_response.first['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET /projects/:id/boards/:board_id/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns a list' do - get api("#{base_url}/#{dev_list.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(dev_list.id) - expect(json_response['label']['name']).to eq(dev_label.title) - expect(json_response['position']).to eq(1) - end - - it 'returns 404 if list not found' do - get api("#{base_url}/5324", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/board/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + describe "POST /projects/:id/boards/lists" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } it 'creates a new issue board list for group labels' do group = create(:group) group_label = create(:group_label, group: group) - project.update(group: group) + board_parent.update(group: group) - post api(base_url, user), label_id: group_label.id + post api(url, user), label_id: group_label.id expect(response).to have_gitlab_http_status(201) expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['position']).to eq(3) end - - it 'creates a new issue board list for project labels' do - post api(base_url, user), label_id: ux_label.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['label']['name']).to eq(ux_label.title) - expect(json_response['position']).to eq(3) - end - - it 'returns 400 when creating a new list if label_id is invalid' do - post api(base_url, user), label_id: 23423 - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 403 for project members with guest role' do - put api("#{base_url}/#{test_list.id}", guest), position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "updates a list" do - put api("#{base_url}/#{test_list.id}", user), - position: 1 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['position']).to eq(1) - end - - it "returns 404 error if list id not found" do - put api("#{base_url}/44444", user), - position: 1 - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 403 for project members with guest role" do - put api("#{base_url}/#{test_list.id}", guest), - position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(204) - end - - it_behaves_like '412 response' do - let(:request) { api("#{base_url}/#{dev_list.id}", owner) } - end - end end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index ffa17d296e8..f246bb79ab7 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -142,6 +142,7 @@ describe API::CommitStatuses do expect(json_response['ref']).not_to be_empty expect(json_response['target_url']).to be_nil expect(json_response['description']).to be_nil + if status == 'failed' expect(CommitStatus.find(json_response['id'])).to be_api_failure end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 0d2bd3207c0..ff5f207487b 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -62,7 +62,7 @@ describe API::Commits do context "since optional parameter" do it "returns project commits since provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) after = commits.second.created_at get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) @@ -73,7 +73,7 @@ describe API::Commits do end it 'include correct pagination headers' do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) after = commits.second.created_at commit_count = project.repository.count_commits(ref: 'master', after: after).to_s @@ -87,12 +87,12 @@ describe API::Commits do context "until optional parameter" do it "returns project commits until provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 20) before = commits.second.created_at get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size >= 20 + if commits.size == 20 expect(json_response.size).to eq(20) else expect(json_response.size).to eq(commits.size - 1) @@ -103,7 +103,7 @@ describe API::Commits do end it 'include correct pagination headers' do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) before = commits.second.created_at commit_count = project.repository.count_commits(ref: 'master', before: before).to_s @@ -181,7 +181,7 @@ describe API::Commits do let(:page) { 3 } it 'returns the third 5 commits' do - commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first expect(json_response.size).to eq(per_page) expect(json_response.first['id']).to eq(commit.id) @@ -512,6 +512,31 @@ describe API::Commits do end end + context 'when stat param' do + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end + context 'when unauthenticated', 'and project is public' do let(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 1f1e6ea17e4..0772b3f2e64 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -110,7 +110,7 @@ describe API::DeployKeys do end it 'accepts can_push parameter' do - key_attrs = attributes_for :write_access_key + key_attrs = attributes_for(:another_key).merge(can_push: true) post api("/projects/#{project.id}/deploy_keys", admin), key_attrs @@ -160,16 +160,6 @@ describe API::DeployKeys do expect(json_response['title']).to eq('new title') expect(json_response['can_push']).to eq(true) end - - it 'updates a private ssh key from projects user has access with correct attributes' do - create(:deploy_keys_project, project: project2, deploy_key: private_deploy_key) - - put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } - - expect(json_response['id']).to eq(private_deploy_key.id) - expect(json_response['title']).to eq('new title') - expect(json_response['can_push']).to eq(true) - end end describe 'DELETE /projects/:id/deploy_keys/:key_id' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index c7977e624ff..51b70fda148 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -3,24 +3,65 @@ require 'spec_helper' describe API::Deployments do let(:user) { create(:user) } let(:non_member) { create(:user) } - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } before do - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/deployments' do + let(:project) { create(:project) } + let!(:deployment_1) { create(:deployment, project: project, iid: 11, ref: 'master', created_at: Time.now) } + let!(:deployment_2) { create(:deployment, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } + let!(:deployment_3) { create(:deployment, project: project, iid: 8, ref: 'feature', created_at: 2.days.ago) } + context 'as member of the project' do - it 'returns projects deployments' do + it 'returns projects deployments sorted by id asc' do get api("/projects/#{project.id}/deployments", 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.size).to eq(1) - expect(json_response.first['iid']).to eq(deployment.iid) + expect(json_response.size).to eq(3) + expect(json_response.first['iid']).to eq(deployment_1.iid) expect(json_response.first['sha']).to match /\A\h{40}\z/ + expect(json_response.second['iid']).to eq(deployment_2.iid) + expect(json_response.last['iid']).to eq(deployment_3.iid) + end + + describe 'ordering' do + using RSpec::Parameterized::TableSyntax + + let(:order_by) { nil } + let(:sort) { nil } + + subject { get api("/projects/#{project.id}/deployments?order_by=#{order_by}&sort=#{sort}", user) } + + def expect_deployments(ordered_deployments) + json_response.each_with_index do |deployment_json, index| + expect(deployment_json['id']).to eq(public_send(ordered_deployments[index]).id) + end + end + + before do + subject + end + + where(:order_by, :sort, :ordered_deployments) do + 'created_at' | 'asc' | [:deployment_3, :deployment_2, :deployment_1] + 'created_at' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3] + 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1] + 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2] + 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3] + 'ref' | 'asc' | [:deployment_2, :deployment_3, :deployment_1] + 'ref' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + end + + with_them do + it 'returns the deployments ordered' do + expect_deployments(ordered_deployments) + end + end end end @@ -34,6 +75,9 @@ describe API::Deployments do end describe 'GET /projects/:id/deployments/:deployment_id' do + let(:project) { deployment.environment.project } + let!(:deployment) { create(:deployment) } + context 'as a member of the project' do it 'returns the projects deployment' do get api("/projects/#{project.id}/deployments/#{deployment.id}", user) diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 3665cfd7241..53d48a91007 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -7,7 +7,7 @@ describe API::Environments do let!(:environment) { create(:environment, project: project) } before do - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/environments' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 5d8338a3fb7..d8fdfd6dee1 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -14,7 +14,7 @@ describe API::Files do let(:author_name) { 'John Doe' } before do - project.team << [user, :developer] + project.add_developer(user) end def route(file_path = nil) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 6330c140246..bb0034e3237 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -30,6 +30,21 @@ describe API::Groups do expect(json_response) .to satisfy_one { |group| group['name'] == group1.name } end + + it 'avoids N+1 queries' do + # Establish baseline + get api("/groups", admin) + + control = ActiveRecord::QueryRecorder.new do + get api("/groups", admin) + end + + create(:group) + + expect do + get api("/groups", admin) + end.not_to exceed_query_limit(control) + end end context "when authenticated as user" do @@ -417,7 +432,7 @@ describe API::Groups do end it "only returns projects to which user has access" do - project3.team << [user3, :developer] + project3.add_developer(user3) get api("/groups/#{group1.id}/projects", user3) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 0462f494e15..837389451e8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -68,6 +68,12 @@ describe API::Helpers do end it { is_expected.to eq(user) } + + it 'sets the environment with data of the current user' do + subject + + expect(env[API::Helpers::API_USER_ENV]).to eq({ user_id: subject.id, username: subject.username }) + end end context "HEAD request" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 3c31980b273..884a258fd12 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -147,7 +147,7 @@ describe API::Internal do describe "POST /internal/lfs_authenticate" do before do - project.team << [user, :developer] + project.add_developer(user) end context 'user key' do @@ -192,6 +192,54 @@ describe API::Internal do end end + describe "GET /internal/authorized_keys" do + context "using an existing key's fingerprint" do + it "finds the key" do + get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + end + + context "non existing key's fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "no:t-:va:li:d0", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "using a partial fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "#{key.fingerprint[0..5]}%", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "sending the key" do + it "finds the key" do + get(api('/internal/authorized_keys'), key: key.key.split[1], secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + + it "returns 404 with a partial key" do + get(api('/internal/authorized_keys'), key: key.key.split[1][0...-3], secret_token: secret_token) + + expect(response.status).to eq(404) + end + + it "returns 404 with an not valid base64 string" do + get(api('/internal/authorized_keys'), key: "whatever!", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + end + describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do context "access granted" do around do |example| @@ -199,7 +247,7 @@ describe API::Internal do end before do - project.team << [user, :developer] + project.add_developer(user) end context 'with env passed as a JSON' do @@ -269,35 +317,20 @@ describe API::Internal do end context "git pull" do - context "gitaly disabled", :disable_gitaly do - it "has the correct payload" do - pull(key, project) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) - expect(json_response["gl_repository"]).to eq("project-#{project.id}") - expect(json_response["gitaly"]).to be_nil - expect(user).to have_an_activity_record - end - end - - context "gitaly enabled" do - it "has the correct payload" do - pull(key, project) + it "has the correct payload" do + pull(key, project) - expect(response).to have_gitlab_http_status(200) - expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) - expect(json_response["gl_repository"]).to eq("project-#{project.id}") - expect(json_response["gitaly"]).not_to be_nil - expect(json_response["gitaly"]["repository"]).not_to be_nil - expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) - expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) - expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) - expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(user).to have_an_activity_record - end + expect(response).to have_gitlab_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gitaly"]).not_to be_nil + expect(json_response["gitaly"]["repository"]).not_to be_nil + expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) + expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) + expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) + expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) + expect(user).to have_an_activity_record end end @@ -359,7 +392,7 @@ describe API::Internal do context "access denied" do before do - project.team << [user, :guest] + project.add_guest(user) end context "git pull" do @@ -413,7 +446,7 @@ describe API::Internal do context "archived project" do before do - project.team << [user, :developer] + project.add_developer(user) project.archive! end @@ -527,7 +560,7 @@ describe API::Internal do context 'web actions are always allowed' do it 'allows WEB push' do stub_application_setting(enabled_git_access_protocol: 'ssh') - project.team << [user, :developer] + project.add_developer(user) push(key, project, 'web') expect(response.status).to eq(200) @@ -540,7 +573,7 @@ describe API::Internal do let!(:repository) { project.repository } before do - project.team << [user, :developer] + project.add_developer(user) project.path = 'new_path' project.save! end @@ -566,7 +599,7 @@ describe API::Internal do let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") } before do - project.team << [user, :developer] + project.add_developer(user) end it 'returns link to create new merge request' do @@ -701,7 +734,7 @@ describe API::Internal do end before do - project.team << [user, :developer] + project.add_developer(user) allow(described_class).to receive(:identify).and_return(user) allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user) end @@ -784,6 +817,16 @@ describe API::Internal do expect(json_response["redirected_message"]).to eq(project_moved.redirect_message) end end + + context 'with an orphaned write deploy key' do + it 'does not try to notify that project moved' do + allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil) + + post api("/internal/post_receive"), valid_params + + expect(response).to have_gitlab_http_status(200) + end + end end describe 'POST /internal/pre_receive' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 3f5070a1fd2..13db40d21a5 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -58,8 +58,8 @@ describe API::Issues, :mailer do let(:no_milestone_title) { URI.escape(Milestone::None.title) } before(:all) do - project.team << [user, :reporter] - project.team << [guest, :guest] + project.add_reporter(user) + project.add_guest(guest) end describe "GET /issues" do @@ -344,7 +344,7 @@ describe API::Issues, :mailer do let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } before do - group_project.team << [user, :reporter] + group_project.add_reporter(user) end let(:base_url) { "/groups/#{group.id}/issues" } @@ -847,6 +847,15 @@ describe API::Issues, :mailer do expect(json_response['assignee']['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name) end + + it 'creates a new project issue when assignee_id is empty' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', assignee_id: '' + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['assignee']).to be_nil + end end context 'single assignee restrictions' do @@ -967,7 +976,7 @@ describe API::Issues, :mailer do let(:project) { merge_request.source_project } before do - project.team << [user, :master] + project.add_master(user) end context 'resolving all discussions in a merge request' do @@ -1432,7 +1441,7 @@ describe API::Issues, :mailer do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/123/issues/#{issue.iid}/move", user), + post api("/projects/12345/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_gitlab_http_status(404) @@ -1443,7 +1452,7 @@ describe API::Issues, :mailer do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - to_project_id: 123 + to_project_id: 12345 expect(response).to have_gitlab_http_status(404) end @@ -1582,4 +1591,16 @@ describe API::Issues, :mailer do expect(json_response).to be_an Array expect(json_response.length).to eq(size) if size end + + describe 'GET projects/:id/issues/:issue_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { issue } + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 2a83213e87a..f8d0b63afec 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -11,7 +11,7 @@ describe API::Jobs do ref: project.default_branch) end - let!(:job) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, :success, pipeline: pipeline) } let(:user) { create(:user) } let(:api_user) { user } @@ -25,8 +25,10 @@ describe API::Jobs do describe 'GET /projects/:id/jobs' do let(:query) { Hash.new } - before do - get api("/projects/#{project.id}/jobs", api_user), query + before do |example| + unless example.metadata[:skip_before_request] + get api("/projects/#{project.id}/jobs", api_user), query + end end context 'authorized user' do @@ -51,6 +53,23 @@ describe API::Jobs do expect(json_job['pipeline']['status']).to eq job.pipeline.status end + it 'avoids N+1 queries', :skip_before_request do + first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build.runner = create(:ci_runner) + first_build.user = create(:user) + first_build.save + + control_count = ActiveRecord::QueryRecorder.new { go }.count + + second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) + second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build.runner = create(:ci_runner) + second_build.user = create(:user) + second_build.save + + expect { go }.not_to exceed_query_limit(control_count) + end + context 'filter project with one scope element' do let(:query) { { 'scope' => 'pending' } } @@ -83,6 +102,10 @@ describe API::Jobs do expect(response).to have_gitlab_http_status(401) end end + + def go + get api("/projects/#{project.id}/jobs", api_user), query + end end describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do @@ -277,44 +300,53 @@ describe API::Jobs do end describe 'GET /projects/:id/jobs/:job_id/artifacts' do - before do - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + shared_examples 'downloads artifact' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end + + it 'returns specific job artifacts' do + expect(response).to have_gitlab_http_status(200) + expect(response.headers).to include(download_headers) + expect(response.body).to match_file(job.artifacts_file.file.file) + end end - context 'job with artifacts' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + context 'normal authentication' do + context 'job with artifacts' do + context 'when artifacts are stored locally' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } - context 'authorized user' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } - end + before do + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end - it 'returns specific job artifacts' do - expect(response).to have_gitlab_http_status(200) - expect(response.headers).to include(download_headers) - expect(response.body).to match_file(job.artifacts_file.file.file) + context 'authorized user' do + it_behaves_like 'downloads artifact' + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job artifacts' do + expect(response).to have_gitlab_http_status(404) + end + end end - end - context 'when anonymous user is accessing private artifacts' do - let(:api_user) { nil } + it 'does not return job artifacts if not uploaded' do + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - it 'hides artifacts and rejects request' do - expect(project).to be_private expect(response).to have_gitlab_http_status(404) end end end - - it 'does not return job artifacts if not uploaded' do - expect(response).to have_gitlab_http_status(404) - end end describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do let(:api_user) { reporter } - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } before do job.success @@ -373,14 +405,16 @@ describe API::Jobs do context 'find proper job' do shared_examples 'a valid file' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - "attachment; filename=#{job.artifacts_file.filename}" } - end + context 'when artifacts are stored locally' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{job.artifacts_file.filename}" } + end - it { expect(response).to have_gitlab_http_status(200) } - it { expect(response.headers).to include(download_headers) } + it { expect(response).to have_gitlab_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end end context 'with regular branch' do @@ -443,7 +477,7 @@ describe API::Jobs do context 'user with :update_build persmission' do it 'cancels running or pending job' do expect(response).to have_gitlab_http_status(201) - expect(project.builds.first.status).to eq('canceled') + expect(project.builds.first.status).to eq('success') end end @@ -503,7 +537,7 @@ describe API::Jobs do let(:role) { :master } before do - project.team << [user, role] + project.add_role(user, role) post api("/projects/#{project.id}/jobs/#{job.id}/erase", user) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 3498e5bc8d9..34cbf75f4c1 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -7,7 +7,7 @@ describe API::Labels do let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } before do - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/labels' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 3349e396ab8..ec500838eb2 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -8,8 +8,8 @@ describe API::Members do let(:project) do create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| - project.team << [developer, :developer] - project.team << [master, :master] + project.add_developer(developer) + project.add_master(master) project.request_access(access_requester) end end @@ -44,6 +44,21 @@ describe API::Members do end end + it 'avoids N+1 queries' do + # Establish baseline + get api("/#{source_type.pluralize}/#{source.id}/members", master) + + control = ActiveRecord::QueryRecorder.new do + get api("/#{source_type.pluralize}/#{source.id}/members", master) + end + + project.add_developer(create(:user)) + + expect do + get api("/#{source_type.pluralize}/#{source.id}/members", master) + end.not_to exceed_query_limit(control) + end + it 'does not return invitees' do create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) @@ -65,6 +80,16 @@ describe API::Members do expect(json_response.count).to eq(1) expect(json_response.first['username']).to eq(master.username) end + + it 'finds all members with no query specified' do + get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: '' + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end end end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index bf4c8443b23..cb647aee70f 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -8,7 +8,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do before do merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 60dbd74d59d..14dd9da119d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -25,7 +25,7 @@ describe API::MergeRequests do let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) } before do - project.team << [user, :reporter] + project.add_reporter(user) end describe 'GET /merge_requests' do @@ -198,6 +198,8 @@ describe API::MergeRequests do create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + expect do get api("/projects/#{project.id}/merge_requests", user) end.not_to exceed_query_limit(control) @@ -500,6 +502,12 @@ describe API::MergeRequests do end end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { merge_request } + end + end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do it 'returns a 200 when merge request is valid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) @@ -545,6 +553,49 @@ describe API::MergeRequests do end end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do + context 'when authorized' do + let!(:pipeline) { create(:ci_empty_pipeline, project: project, user: user, ref: merge_request.source_branch, sha: merge_request.diff_head_sha) } + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + + it 'returns a paginated array of corresponding pipelines' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(pipeline.id) + end + + it 'exposes basic attributes' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pipelines') + end + + it 'returns 404 if MR does not exist' do + get api("/projects/#{project.id}/merge_requests/777/pipelines") + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when unauthorized' do + it 'returns 403' do + project = create(:project, public_builds: false) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.add_guest(guest) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines", guest) + + expect(response).to have_gitlab_http_status(403) + end + end + end + describe "POST /projects/:id/merge_requests" do context 'between branches projects' do it "returns merge_request" do @@ -705,16 +756,28 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(400) end - context 'when target_branch is specified' do + context 'when target_branch and target_project_id is specified' do + let(:params) do + { title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id } + end + it 'returns 422 if targeting a different fork' do - post api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id + unrelated_project.add_developer(user2) + + post api("/projects/#{forked_project.id}/merge_requests", user2), params + expect(response).to have_gitlab_http_status(422) end + + it 'returns 403 if targeting a different fork which user can not access' do + post api("/projects/#{forked_project.id}/merge_requests", user2), params + + expect(response).to have_gitlab_http_status(403) + end end it "returns 201 when target_branch is specified and for the same project" do @@ -730,7 +793,7 @@ describe API::MergeRequests do let(:developer) { create(:user) } before do - project.team << [developer, :developer] + project.add_developer(developer) end it "denies the deletion of the merge request" do @@ -808,7 +871,7 @@ describe API::MergeRequests do it "returns 401 if user has no permissions to merge" do user2 = create(:user) - project.team << [user2, :reporter] + project.add_reporter(user2) put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2) expect(response).to have_gitlab_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') @@ -997,7 +1060,7 @@ describe API::MergeRequests do project = create(:project, :private) merge_request = create(:merge_request, :simple, source_project: project) guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest) @@ -1045,7 +1108,7 @@ describe API::MergeRequests do it 'returns 403 if user has no access to read code' do guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest) @@ -1081,7 +1144,7 @@ describe API::MergeRequests do it 'returns 403 if user has no access to read code' do guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 3bfb4c5506f..981c9c27325 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -14,7 +14,7 @@ describe API::Notes do let(:private_user) { create(:user) } let(:private_project) do create(:project, namespace: private_user.namespace) - .tap { |p| p.team << [private_user, :master] } + .tap { |p| p.add_master(private_user) } end let(:private_issue) { create(:issue, project: private_project) } @@ -29,7 +29,7 @@ describe API::Notes do end before do - project.team << [user, :reporter] + project.add_reporter(user) end describe "GET /projects/:id/noteable/:noteable_id/notes" do @@ -464,7 +464,7 @@ describe API::Notes do describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' end diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index d412b045e9f..5d01dc37f0e 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -46,6 +46,7 @@ describe API::PagesDomains do expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.last).to have_key('domain') + expect(json_response.last).to have_key('project_id') expect(json_response.last).to have_key('certificate_expiration') expect(json_response.last['certificate_expiration']['expired']).to be true expect(json_response.first).not_to have_key('certificate_expiration') diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index e4dcc9252fa..0736329f9fd 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -11,7 +11,7 @@ describe API::Pipelines do end before do - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/pipelines ' do @@ -424,7 +424,7 @@ describe API::Pipelines do let!(:reporter) { create(:user) } before do - project.team << [reporter, :reporter] + project.add_reporter(reporter) end it 'rejects the action' do diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index f31344a6238..1fd082ecc38 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -13,8 +13,8 @@ describe API::ProjectHooks, 'ProjectHooks' do end before do - project.team << [user, :master] - project.team << [user3, :developer] + project.add_master(user) + project.add_developer(user3) end describe "GET /projects/:id/hooks" do @@ -206,7 +206,7 @@ describe API::ProjectHooks, 'ProjectHooks' do it "returns a 404 if a user attempts to delete project hooks he/she does not own" do test_user = create(:user) other_project = create(:project) - other_project.team << [test_user, :master] + other_project.add_master(test_user) delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) expect(response).to have_gitlab_http_status(404) diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 72e1574b55f..6c05c166bd6 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -7,16 +7,56 @@ describe API::ProjectMilestones do let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } before do - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like 'group and project milestones', "/projects/:id/milestones" do let(:route) { "/projects/#{project.id}/milestones" } end + describe 'DELETE /projects/:id/milestones/:milestone_id' do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + + before do + project.add_reporter(reporter) + end + + it 'returns 404 response when the project does not exists' do + delete api("/projects/999/milestones/#{milestone.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 response when the milestone does not exists' do + delete api("/projects/#{project.id}/milestones/999", user) + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 404 from guest user deleting a milestone" do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", guest) + + expect(response).to have_gitlab_http_status(404) + end + + it "rejects a member with reporter access from deleting a milestone" do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", reporter) + + expect(response).to have_gitlab_http_status(403) + end + + it 'deletes the milestone when the user has developer access to the project' do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(project.milestones.find_by_id(milestone.id)).to be_nil + expect(response).to have_gitlab_http_status(204) + end + end + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index e741ac4b7bd..4a2289ca137 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' describe API::ProjectSnippets do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:project) { create(:project, :public) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do let(:snippet) { create(:project_snippet, :public, project: project) } @@ -18,6 +18,13 @@ describe API::ProjectSnippets do expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) end + it 'respects project scoping' do + other_project = create(:project) + + get api("/projects/#{other_project.id}/snippets/#{snippet.id}/user_agent_detail", admin) + expect(response).to have_gitlab_http_status(404) + end + it "returns unautorized for non-admin users" do get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a41345da05b..f11cd638d96 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2,8 +2,6 @@ require 'spec_helper' describe API::Projects do - include Gitlab::CurrentSettings - let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -150,6 +148,19 @@ describe API::Projects do expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') end + context 'and with_issues_enabled=true' do + it 'only returns projects with issues enabled' do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + + get api('/projects?with_issues_enabled=true', user) + + expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).not_to include(project.id) + end + end + it "does not include statistics by default" do get api('/projects', user) @@ -352,6 +363,19 @@ describe API::Projects do let(:current_user) { user2 } let(:projects) { [public_project] } end + + context 'and with_issues_enabled=true' do + it 'does not return private issue projects' do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) + + get api('/projects?with_issues_enabled=true', user2) + + expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).not_to include(project.id) + end + end end context 'when authenticated as admin' do @@ -908,7 +932,7 @@ describe API::Projects do describe 'permissions' do context 'all projects' do before do - project.team << [user, :master] + project.add_master(user) end it 'contains permission information' do @@ -923,7 +947,7 @@ describe API::Projects do context 'personal project' do it 'sets project access and returns 200' do - project.team << [user, :master] + project.add_master(user) get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(200) @@ -1539,7 +1563,7 @@ describe API::Projects do context 'user without archiving rights to the project' do before do - project.team << [user3, :developer] + project.add_developer(user3) end it 'rejects the action' do @@ -1575,7 +1599,7 @@ describe API::Projects do context 'user without archiving rights to the project' do before do - project.team << [user3, :developer] + project.add_developer(user3) end it 'rejects the action' do @@ -1650,7 +1674,7 @@ describe API::Projects do it 'does not remove a project if not an owner' do user3 = create(:user) - project.team << [user3, :developer] + project.add_developer(user3) delete api("/projects/#{project.id}", user3) expect(response).to have_gitlab_http_status(403) end diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 10e6a3c07c8..1d23e023bb6 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -80,6 +80,12 @@ describe API::ProtectedBranches do it_behaves_like 'protected branch' end + + context 'when protected branch contains a period' do + let(:protected_name) { 'my.feature' } + + it_behaves_like 'protected branch' + end end context 'when authenticated as a guest' do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 679d391caa5..c5c0b0c2867 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -945,7 +945,7 @@ describe API::Runner do context 'when artifacts are being stored inside of tmp path' do before do # by configuring this path we allow to pass temp file from any path - allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return('/') + allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/') end context 'when job has been erased' do @@ -1122,7 +1122,7 @@ describe API::Runner do # by configuring this path we allow to pass file from @tmpdir only # but all temporary files are stored in system tmp directory @tmpdir = Dir.mktmpdir - allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) + allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return(@tmpdir) end after do @@ -1142,6 +1142,7 @@ describe API::Runner do else { 'file' => file } end + post api("/jobs/#{job.id}/artifacts"), params, headers end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index ba697e2b305..236f8d7faf5 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -53,6 +53,10 @@ describe API::Services do describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service + before do + initialize_service(service) + end + it "deletes #{service}" do delete api("/projects/#{project.id}/services/#{dashed_service}", user) @@ -67,9 +71,7 @@ describe API::Services do # inject some properties into the service before do - service_object = project.find_or_initialize_service(service) - service_object.properties = service_attrs - service_object.save + initialize_service(service) end it 'returns authentication error when unauthenticated' do @@ -81,18 +83,18 @@ describe API::Services do get api("/projects/#{project.id}/services/#{dashed_service}", admin) expect(response).to have_gitlab_http_status(200) - expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map) + expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) end it "returns properties of service #{service} other than passwords when authenticated as project owner" do get api("/projects/#{project.id}/services/#{dashed_service}", user) expect(response).to have_gitlab_http_status(200) - expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords) + expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) end it "returns error when authenticated but not a project owner" do - project.team << [user2, :developer] + project.add_developer(user2) get api("/projects/#{project.id}/services/#{dashed_service}", user2) expect(response).to have_gitlab_http_status(403) diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index c7a009e999e..6c57d443cbf 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -36,6 +36,7 @@ describe API::SystemHooks do expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be false expect(json_response.first['tag_push_events']).to be false + expect(json_response.first['merge_requests_events']).to be false expect(json_response.first['repository_update_events']).to be true end end @@ -67,11 +68,28 @@ describe API::SystemHooks do end it 'sets default values for events' do - post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true + post api('/hooks', admin), url: 'http://mep.mep' expect(response).to have_gitlab_http_status(201) expect(json_response['enable_ssl_verification']).to be true + expect(json_response['push_events']).to be false expect(json_response['tag_push_events']).to be false + expect(json_response['merge_requests_events']).to be false + end + + it 'sets explicit values for events' do + post api('/hooks', admin), + url: 'http://mep.mep', + enable_ssl_verification: false, + push_events: true, + tag_push_events: true, + merge_requests_events: true + + expect(response).to have_http_status(201) + expect(json_response['enable_ssl_verification']).to be false + expect(json_response['push_events']).to be true + expect(json_response['tag_push_events']).to be true + expect(json_response['merge_requests_events']).to be true end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index c6063a2e089..fb3a33cadff 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -13,8 +13,8 @@ describe API::Todos do let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } before do - project_1.team << [john_doe, :developer] - project_2.team << [john_doe, :developer] + project_1.add_developer(john_doe) + project_2.add_developer(john_doe) end describe 'GET /todos' do @@ -191,7 +191,7 @@ describe API::Todos do it 'returns an error if the issuable is not accessible' do guest = create(:user) - project_1.team << [guest, :guest] + project_1.add_guest(guest) post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest) diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb index 0cd8b70007f..6dc430676b0 100644 --- a/spec/requests/api/v3/award_emoji_spec.rb +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -9,7 +9,7 @@ describe API::V3::AwardEmoji do let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } set(:note) { create(:note, project: project, noteable: issue) } - before { project.team << [user, :master] } + before { project.add_master(user) } describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb index 14409d25544..dde4f096193 100644 --- a/spec/requests/api/v3/boards_spec.rb +++ b/spec/requests/api/v3/boards_spec.rb @@ -27,8 +27,8 @@ describe API::V3::Boards do end before do - project.team << [user, :reporter] - project.team << [guest, :guest] + project.add_reporter(user) + project.add_guest(guest) end describe "GET /projects/:id/boards" do diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index a73bb456b52..3f92288fef0 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -4,19 +4,23 @@ describe API::V3::Builds do set(:user) { create(:user) } let(:api_user) { user } set(:project) { create(:project, :repository, creator: user, public_builds: false) } - set(:developer) { create(:project_member, :developer, user: user, project: project) } - set(:reporter) { create(:project_member, :reporter, project: project) } - set(:guest) { create(:project_member, :guest, project: project) } - set(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:developer) { create(:project_member, :developer, user: user, project: project) } + let(:reporter) { create(:project_member, :reporter, project: project) } + let(:guest) { create(:project_member, :guest, project: project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + let(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } - before do + before do |example| + build + create(:ci_build, :skipped, pipeline: pipeline) - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + unless example.metadata[:skip_before_request] + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + end end context 'authorized user' do @@ -40,6 +44,23 @@ describe API::V3::Builds do expect(json_build['pipeline']['status']).to eq build.pipeline.status end + it 'avoids N+1 queries', :skip_before_request do + first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build.runner = create(:ci_runner) + first_build.user = create(:user) + first_build.save + + control_count = ActiveRecord::QueryRecorder.new { go }.count + + second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) + second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build.runner = create(:ci_runner) + second_build.user = create(:user) + second_build.save + + expect { go }.not_to exceed_query_limit(control_count) + end + context 'filter project with one scope element' do let(:query) { 'scope=pending' } @@ -84,9 +105,17 @@ describe API::V3::Builds do expect(response).to have_gitlab_http_status(401) end end + + def go + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + end end describe 'GET /projects/:id/repository/commits/:sha/builds' do + before do + build + end + context 'when commit does not exist in repository' do before do get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) @@ -191,18 +220,20 @@ describe API::V3::Builds do end context 'job with artifacts' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + context 'when artifacts are stored locally' do + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - context 'authorized user' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } - end + context 'authorized user' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end - it 'returns specific job artifacts' do - expect(response).to have_gitlab_http_status(200) - expect(response.headers).to include(download_headers) - expect(response.body).to match_file(build.artifacts_file.file.file) + it 'returns specific job artifacts' do + expect(response).to have_gitlab_http_status(200) + expect(response.headers).to include(download_headers) + expect(response.body).to match_file(build.artifacts_file.file.file) + end end end @@ -280,14 +311,16 @@ describe API::V3::Builds do context 'find proper job' do shared_examples 'a valid file' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } - end + context 'when artifacts are stored locally' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + end - it { expect(response).to have_gitlab_http_status(200) } - it { expect(response.headers).to include(download_headers) } + it { expect(response).to have_gitlab_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end end context 'with regular branch' do diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index d31c94ddd2c..9ef3b859001 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -9,11 +9,11 @@ describe API::V3::Commits do let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } - before { project.team << [user, :reporter] } + before { project.add_reporter(user) } describe "List repository commits" do context "authorized user" do - before { project.team << [user2, :reporter] } + before { project.add_reporter(user2) } it "returns project commits" do commit = project.repository.commit @@ -36,7 +36,7 @@ describe API::V3::Commits do context "since optional parameter" do it "returns project commits since provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) since = commits.second.created_at get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) @@ -49,12 +49,12 @@ describe API::V3::Commits do context "until optional parameter" do it "returns project commits until provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 20) before = commits.second.created_at get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size >= 20 + if commits.size == 20 expect(json_response.size).to eq(20) else expect(json_response.size).to eq(commits.size - 1) @@ -403,6 +403,33 @@ describe API::V3::Commits do expect(response).to have_gitlab_http_status(200) expect(json_response['status']).to eq("created") end + + context 'when stat param' do + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get v3_api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get v3_api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get v3_api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end end context "unauthorized user" do @@ -415,7 +442,7 @@ describe API::V3::Commits do describe "Get the diff of a commit" do context "authorized user" do - before { project.team << [user2, :reporter] } + before { project.add_reporter(user2) } it "returns the diff of the selected commit" do get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) @@ -487,7 +514,7 @@ describe API::V3::Commits do end it 'returns 400 if you are not allowed to push to the target branch' do - project.team << [user2, :developer] + project.add_developer(user2) protected_branch = create(:protected_branch, project: project, name: 'feature') post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb index 785bc1eb4ba..501af587ad4 100644 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ b/spec/requests/api/v3/deploy_keys_spec.rb @@ -107,7 +107,7 @@ describe API::V3::DeployKeys do end it 'accepts can_push parameter' do - key_attrs = attributes_for :write_access_key + key_attrs = attributes_for(:another_key).merge(can_push: true) post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb index 90eabda4dac..ac86fbea498 100644 --- a/spec/requests/api/v3/deployments_spec.rb +++ b/spec/requests/api/v3/deployments_spec.rb @@ -7,7 +7,7 @@ describe API::V3::Deployments do let!(:deployment) { create(:deployment) } before do - project.team << [user, :master] + project.add_master(user) end shared_examples 'a paginated resources' do diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb index 937250b5219..68be5256b64 100644 --- a/spec/requests/api/v3/environments_spec.rb +++ b/spec/requests/api/v3/environments_spec.rb @@ -7,7 +7,7 @@ describe API::V3::Environments do let!(:environment) { create(:environment, project: project) } before do - project.team << [user, :master] + project.add_master(user) end shared_examples 'a paginated resources' do diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 5500c1cf770..26a3d8870a0 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -27,7 +27,7 @@ describe API::V3::Files do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } - before { project.team << [user, :developer] } + before { project.add_developer(user) } describe "GET /projects/:id/repository/files" do let(:route) { "/projects/#{project.id}/repository/files" } diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 498cb42fad1..a1cdf583de3 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -330,7 +330,7 @@ describe API::V3::Groups do end it "only returns projects to which user has access" do - project3.team << [user3, :developer] + project3.add_developer(user3) get v3_api("/groups/#{group1.id}/projects", user3) diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 39a47a62f16..0dd6d673625 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -50,8 +50,8 @@ describe API::V3::Issues, :mailer do let(:no_milestone_title) { URI.escape(Milestone::None.title) } before do - project.team << [user, :reporter] - project.team << [guest, :guest] + project.add_reporter(user) + project.add_guest(guest) end describe "GET /issues" do @@ -278,7 +278,7 @@ describe API::V3::Issues, :mailer do let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } before do - group_project.team << [user, :reporter] + group_project.add_reporter(user) end let(:base_url) { "/groups/#{group.id}/issues" } @@ -827,7 +827,7 @@ describe API::V3::Issues, :mailer do let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } before do - project.team << [user, :master] + project.add_master(user) post v3_api("/projects/#{project.id}/issues", user), title: 'New Issue', merge_request_for_resolving_discussions: merge_request.iid diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb index 1d31213d5ca..cdab4d2bd73 100644 --- a/spec/requests/api/v3/labels_spec.rb +++ b/spec/requests/api/v3/labels_spec.rb @@ -7,7 +7,7 @@ describe API::V3::Labels do let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } before do - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/labels' do diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb index 68be3d24c26..de4339ecb8b 100644 --- a/spec/requests/api/v3/members_spec.rb +++ b/spec/requests/api/v3/members_spec.rb @@ -8,8 +8,8 @@ describe API::V3::Members do let(:project) do create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| - project.team << [developer, :developer] - project.team << [master, :master] + project.add_developer(developer) + project.add_master(master) project.request_access(access_requester) end end @@ -58,6 +58,16 @@ describe API::V3::Members do expect(json_response.count).to eq(1) expect(json_response.first['username']).to eq(master.username) end + + it 'finds all members with no query specified' do + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: '' + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end end end diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb index e613036a88d..547c066fadc 100644 --- a/spec/requests/api/v3/merge_request_diffs_spec.rb +++ b/spec/requests/api/v3/merge_request_diffs_spec.rb @@ -8,7 +8,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do before do merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') - project.team << [user, :master] + project.add_master(user) end describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 2e2b9449429..6b748369f0d 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -14,7 +14,7 @@ describe API::MergeRequests do let(:milestone) { create(:milestone, title: '1.0.0', project: project) } before do - project.team << [user, :reporter] + project.add_reporter(user) end describe "GET /projects/:id/merge_requests" do @@ -371,16 +371,28 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(400) end - context 'when target_branch is specified' do + context 'when target_branch and target_project_id is specified' do + let(:params) do + { title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id } + end + it 'returns 422 if targeting a different fork' do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id + unrelated_project.add_developer(user2) + + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params + expect(response).to have_gitlab_http_status(422) end + + it 'returns 403 if targeting a different fork which user can not access' do + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params + + expect(response).to have_gitlab_http_status(403) + end end it "returns 201 when target_branch is specified and for the same project" do @@ -396,7 +408,7 @@ describe API::MergeRequests do let(:developer) { create(:user) } before do - project.team << [developer, :developer] + project.add_developer(developer) end it "denies the deletion of the merge request" do @@ -458,7 +470,7 @@ describe API::MergeRequests do it "returns 401 if user has no permissions to merge" do user2 = create(:user) - project.team << [user2, :reporter] + project.add_reporter(user2) put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) expect(response).to have_gitlab_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') @@ -645,7 +657,7 @@ describe API::MergeRequests do project = create(:project, :private, :repository) merge_request = create(:merge_request, :simple, source_project: project) guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) @@ -675,7 +687,7 @@ describe API::MergeRequests do it 'returns 403 if user has no access to read code' do guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) @@ -705,7 +717,7 @@ describe API::MergeRequests do it 'returns 403 if user has no access to read code' do guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb index e82f35598a6..6021600e09c 100644 --- a/spec/requests/api/v3/milestones_spec.rb +++ b/spec/requests/api/v3/milestones_spec.rb @@ -6,7 +6,7 @@ describe API::V3::Milestones do let!(:closed_milestone) { create(:closed_milestone, project: project) } let!(:milestone) { create(:milestone, project: project) } - before { project.team << [user, :developer] } + before { project.add_developer(user) } describe 'GET /projects/:id/milestones' do it 'returns project milestones' do @@ -161,7 +161,7 @@ describe API::V3::Milestones do describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' @@ -200,7 +200,7 @@ describe API::V3::Milestones do let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } before do - public_project.team << [user, :developer] + public_project.add_developer(user) milestone.issues << issue << confidential_issue end @@ -215,7 +215,7 @@ describe API::V3::Milestones do it 'does not return confidential issues to team members with guest role' do member = create(:user) - project.team << [member, :guest] + project.add_guest(member) get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index d3455a4bba4..5532795ab02 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -14,7 +14,7 @@ describe API::V3::Notes do let(:private_user) { create(:user) } let(:private_project) do create(:project, namespace: private_user.namespace) - .tap { |p| p.team << [private_user, :master] } + .tap { |p| p.add_master(private_user) } end let(:private_issue) { create(:issue, project: private_project) } @@ -28,7 +28,7 @@ describe API::V3::Notes do system: true end - before { project.team << [user, :reporter] } + before { project.add_reporter(user) } describe "GET /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do @@ -302,7 +302,7 @@ describe API::V3::Notes do describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' end diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb index 1c7d9fe32bb..ea943f22c41 100644 --- a/spec/requests/api/v3/pipelines_spec.rb +++ b/spec/requests/api/v3/pipelines_spec.rb @@ -10,7 +10,7 @@ describe API::V3::Pipelines do ref: project.default_branch) end - before { project.team << [user, :master] } + before { project.add_master(user) } shared_examples 'a paginated resources' do before do @@ -188,7 +188,7 @@ describe API::V3::Pipelines do context 'user without proper access rights' do let!(:reporter) { create(:user) } - before { project.team << [reporter, :reporter] } + before { project.add_reporter(reporter) } it 'rejects the action' do post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb index 00f59744a31..248ae97f875 100644 --- a/spec/requests/api/v3/project_hooks_spec.rb +++ b/spec/requests/api/v3/project_hooks_spec.rb @@ -13,8 +13,8 @@ describe API::ProjectHooks, 'ProjectHooks' do end before do - project.team << [user, :master] - project.team << [user3, :developer] + project.add_master(user) + project.add_developer(user3) end describe "GET /projects/:id/hooks" do @@ -205,7 +205,7 @@ describe API::ProjectHooks, 'ProjectHooks' do it "returns a 404 if a user attempts to delete project hooks he/she does not own" do test_user = create(:user) other_project = create(:project) - other_project.team << [test_user, :master] + other_project.add_master(test_user) delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) expect(response).to have_gitlab_http_status(404) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 27288b98d1c..5d99d9495f3 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe API::V3::Projects do - include Gitlab::CurrentSettings - let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -753,7 +751,7 @@ describe API::V3::Projects do describe 'permissions' do context 'all projects' do - before { project.team << [user, :master] } + before { project.add_master(user) } it 'contains permission information' do get v3_api("/projects", user) @@ -767,7 +765,7 @@ describe API::V3::Projects do context 'personal project' do it 'sets project access and returns 200' do - project.team << [user, :master] + project.add_master(user) get v3_api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(200) @@ -1362,7 +1360,7 @@ describe API::V3::Projects do context 'user without archiving rights to the project' do before do - project.team << [user3, :developer] + project.add_developer(user3) end it 'rejects the action' do @@ -1398,7 +1396,7 @@ describe API::V3::Projects do context 'user without archiving rights to the project' do before do - project.team << [user3, :developer] + project.add_developer(user3) end it 'rejects the action' do @@ -1466,7 +1464,7 @@ describe API::V3::Projects do it 'does not remove a project if not an owner' do user3 = create(:user) - project.team << [user3, :developer] + project.add_developer(user3) delete v3_api("/projects/#{project.id}", user3) expect(response).to have_gitlab_http_status(403) end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb index 8f212ab6be6..c69a7d58ca6 100644 --- a/spec/requests/api/v3/services_spec.rb +++ b/spec/requests/api/v3/services_spec.rb @@ -10,6 +10,10 @@ describe API::V3::Services do describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service + before do + initialize_service(service) + end + it "deletes #{service}" do delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb index 8f5c3fbf8dd..53fd962272a 100644 --- a/spec/requests/api/v3/todos_spec.rb +++ b/spec/requests/api/v3/todos_spec.rb @@ -12,8 +12,8 @@ describe API::V3::Todos do let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } before do - project_1.team << [john_doe, :developer] - project_2.team << [john_doe, :developer] + project_1.add_developer(john_doe) + project_2.add_developer(john_doe) end describe 'DELETE /todos/:id' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 65bd001e491..fb0806ff9f1 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -12,6 +12,8 @@ require 'spec_helper' describe API::Wikis do let(:user) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(user) } } + let(:project_wiki) { create(:project_wiki, project: project, user: user) } let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } let(:expected_keys_with_content) { %w(content format slug title) } let(:expected_keys_without_content) { %w(format slug title) } @@ -19,8 +21,8 @@ describe API::Wikis do shared_examples_for 'returns list of wiki pages' do context 'when wiki has pages' do let!(:pages) do - [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }), - create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })] + [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }), + create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })] end it 'returns the list of wiki pages without content' do @@ -445,7 +447,7 @@ describe API::Wikis do end describe 'PUT /projects/:id/wikis/:slug' do - let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:page) { create(:wiki_page, wiki: project_wiki) } let(:payload) { { title: 'new title', content: 'new content' } } let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } @@ -568,10 +570,20 @@ describe API::Wikis do end end end + + context 'when wiki belongs to a group project' do + let(:project) { create(:project, namespace: group) } + + before do + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + end end describe 'DELETE /projects/:id/wikis/:slug' do - let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:page) { create(:wiki_page, wiki: project_wiki) } let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } context 'when wiki is disabled' do @@ -675,5 +687,15 @@ describe API::Wikis do end end end + + context 'when wiki belongs to a group project' do + let(:project) { create(:project, namespace: group) } + + before do + delete(api(url, user)) + end + + include_examples '204 No Content' + end end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index fa02fffc82a..27bd22d6bca 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -149,7 +149,7 @@ describe 'Git HTTP requests' do context 'and as a developer on the team' do before do - project.team << [user, :developer] + project.add_developer(user) end context 'but the repo is disabled' do @@ -182,7 +182,7 @@ describe 'Git HTTP requests' do context 'when authenticated' do context 'and as a developer on the team' do before do - project.team << [user, :developer] + project.add_developer(user) end context 'but the repo is disabled' do @@ -240,7 +240,7 @@ describe 'Git HTTP requests' do context 'as a developer on the team' do before do - project.team << [user, :developer] + project.add_developer(user) end it_behaves_like 'pulls are allowed' @@ -365,13 +365,13 @@ describe 'Git HTTP requests' do context "when the user has access to the project" do before do - project.team << [user, :master] + project.add_master(user) end context "when the user is blocked" do it "rejects pulls with 401 Unauthorized" do user.block - project.team << [user, :master] + project.add_master(user) download(path, env) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -434,7 +434,7 @@ describe 'Git HTTP requests' do let(:path) { "#{project.full_path}.git" } before do - project.team << [user, :master] + project.add_master(user) end context 'when username and password are provided' do @@ -612,7 +612,7 @@ describe 'Git HTTP requests' do context 'and build created by' do before do build.update(user: user) - project.team << [user, :reporter] + project.add_reporter(user) end shared_examples 'can download code only' do @@ -795,7 +795,7 @@ describe 'Git HTTP requests' do context 'and the user is on the team' do before do - project.team << [user, :master] + project.add_master(user) end it "responds with status 200" do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index c597623bc4d..930ef49b7f3 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -63,7 +63,7 @@ describe 'Git LFS API and storage' do context 'with LFS disabled globally' do before do - project.team << [user, :master] + project.add_master(user) allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) end @@ -106,7 +106,7 @@ describe 'Git LFS API and storage' do context 'with LFS enabled globally' do before do - project.team << [user, :master] + project.add_master(user) enable_lfs end @@ -234,7 +234,7 @@ describe 'Git LFS API and storage' do context 'and does have project access' do let(:update_permissions) do - project.team << [user, :master] + project.add_master(user) project.lfs_objects << lfs_object end @@ -259,7 +259,7 @@ describe 'Git LFS API and storage' do context 'when user allowed' do let(:update_permissions) do - project.team << [user, :master] + project.add_master(user) project.lfs_objects << lfs_object end @@ -295,7 +295,7 @@ describe 'Git LFS API and storage' do let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:update_permissions) do - project.team << [user, :reporter] + project.add_reporter(user) project.lfs_objects << lfs_object end @@ -517,7 +517,7 @@ describe 'Git LFS API and storage' do let(:authorization) { authorize_user } let(:update_user_permissions) do - project.team << [user, role] + project.add_role(user, role) end it_behaves_like 'an authorized requests' do @@ -553,7 +553,7 @@ describe 'Git LFS API and storage' do let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:update_user_permissions) do - project.team << [user, :reporter] + project.add_reporter(user) end it_behaves_like 'an authorized requests' @@ -673,7 +673,7 @@ describe 'Git LFS API and storage' do let(:authorization) { authorize_user } let(:update_user_permissions) do - project.team << [user, :developer] + project.add_developer(user) end context 'when pushing an lfs object that already exists' do @@ -781,11 +781,11 @@ describe 'Git LFS API and storage' do end context 'when deploy key has project push access' do - let(:key) { create(:deploy_key, can_push: true) } + let(:key) { create(:deploy_key) } let(:authorization) { authorize_deploy_key } let(:update_user_permissions) do - project.deploy_keys << key + project.deploy_keys_projects.create(deploy_key: key, can_push: true) end it_behaves_like 'pushes new LFS objects' @@ -795,7 +795,7 @@ describe 'Git LFS API and storage' do context 'when user is not authenticated' do context 'when user has push access' do let(:update_user_permissions) do - project.team << [user, :master] + project.add_master(user) end it 'responds with status 401' do @@ -840,7 +840,7 @@ describe 'Git LFS API and storage' do before do allow(Gitlab::Database).to receive(:read_only?) { true } - project.team << [user, :master] + project.add_master(user) enable_lfs end @@ -935,7 +935,7 @@ describe 'Git LFS API and storage' do describe 'when user has push access to the project' do before do - project.team << [user, :developer] + project.add_developer(user) end context 'and the request bypassed workhorse' do @@ -958,7 +958,7 @@ describe 'Git LFS API and storage' do end it 'responds with status 200, location of lfs store and object details' do - expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path) expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end @@ -993,7 +993,7 @@ describe 'Git LFS API and storage' do describe 'and user does not have push access' do before do - project.team << [user, :reporter] + project.add_reporter(user) end it_behaves_like 'forbidden' @@ -1010,7 +1010,7 @@ describe 'Git LFS API and storage' do let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } before do - project.team << [user, :developer] + project.add_developer(user) put_authorize end @@ -1062,7 +1062,7 @@ describe 'Git LFS API and storage' do describe 'when user has push access to the project' do before do - project.team << [user, :developer] + project.add_developer(user) end context 'and request is sent by gitlab-workhorse to authorize the request' do @@ -1075,7 +1075,7 @@ describe 'Git LFS API and storage' do end it 'with location of lfs store and object details' do - expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path) expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end @@ -1149,7 +1149,7 @@ describe 'Git LFS API and storage' do let(:authorization) { authorize_user } before do - second_project.team << [user, :master] + second_project.add_master(user) upstream_project.lfs_objects << lfs_object end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 286d8a884a4..98f70e2101b 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -7,7 +7,7 @@ describe 'cycle analytics events' do describe 'GET /:namespace/:project/cycle_analytics/events/issues' do before do - project.team << [user, :developer] + project.add_developer(user) 3.times do |count| Timecop.freeze(Time.now + count.days) do diff --git a/spec/rubocop/cop/active_record_dependent_spec.rb b/spec/rubocop/cop/active_record_dependent_spec.rb deleted file mode 100644 index 599a032bfc5..00000000000 --- a/spec/rubocop/cop/active_record_dependent_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../rubocop/cop/active_record_dependent' - -describe RuboCop::Cop::ActiveRecordDependent do - include CopHelper - - subject(:cop) { described_class.new } - - context 'inside the app/models directory' do - it 'registers an offense when dependent: is used' do - allow(cop).to receive(:in_model?).and_return(true) - - inspect_source(cop, 'belongs_to :foo, dependent: :destroy') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end - end - - context 'outside the app/models directory' do - it 'does nothing' do - allow(cop).to receive(:in_model?).and_return(false) - - inspect_source(cop, 'belongs_to :foo, dependent: :destroy') - - expect(cop.offenses).to be_empty - end - end -end diff --git a/spec/rubocop/cop/active_record_serialize_spec.rb b/spec/rubocop/cop/active_record_serialize_spec.rb deleted file mode 100644 index b94b25cecd0..00000000000 --- a/spec/rubocop/cop/active_record_serialize_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../rubocop/cop/active_record_serialize' - -describe RuboCop::Cop::ActiveRecordSerialize do - include CopHelper - - subject(:cop) { described_class.new } - - context 'inside the app/models directory' do - it 'registers an offense when serialize is used' do - allow(cop).to receive(:in_model?).and_return(true) - - inspect_source(cop, 'serialize :foo') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end - end - - context 'outside the app/models directory' do - it 'does nothing' do - allow(cop).to receive(:in_model?).and_return(false) - - inspect_source(cop, 'serialize :foo') - - expect(cop.offenses).to be_empty - end - end -end diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb deleted file mode 100644 index 381d7871a40..00000000000 --- a/spec/rubocop/cop/custom_error_class_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../rubocop/cop/custom_error_class' - -describe RuboCop::Cop::CustomErrorClass do - include CopHelper - - subject(:cop) { described_class.new } - - context 'when a class has a body' do - it 'does nothing' do - inspect_source(cop, 'class CustomError < StandardError; def foo; end; end') - - expect(cop.offenses).to be_empty - end - end - - context 'when a class has no explicit superclass' do - it 'does nothing' do - inspect_source(cop, 'class CustomError; end') - - expect(cop.offenses).to be_empty - end - end - - context 'when a class has a superclass that does not end in Error' do - it 'does nothing' do - inspect_source(cop, 'class CustomError < BasicObject; end') - - expect(cop.offenses).to be_empty - end - end - - context 'when a class is empty and inherits from a class ending in Error' do - context 'when the class is on a single line' do - let(:source) do - <<-SOURCE - module Foo - class CustomError < Bar::Baz::BaseError; end - end - SOURCE - end - - let(:expected) do - <<-EXPECTED - module Foo - CustomError = Class.new(Bar::Baz::BaseError) - end - EXPECTED - end - - it 'registers an offense' do - expected_highlights = source.split("\n")[1].strip - - inspect_source(cop, source) - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([2]) - expect(cop.highlights).to contain_exactly(expected_highlights) - end - end - - it 'autocorrects to the right version' do - autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') - - expect(autocorrected).to eq(expected) - end - end - - context 'when the class is on multiple lines' do - let(:source) do - <<-SOURCE - module Foo - class CustomError < Bar::Baz::BaseError - end - end - SOURCE - end - - let(:expected) do - <<-EXPECTED - module Foo - CustomError = Class.new(Bar::Baz::BaseError) - end - EXPECTED - end - - it 'registers an offense' do - expected_highlights = source.split("\n")[1..2].join("\n").strip - - inspect_source(cop, source) - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([2]) - expect(cop.highlights).to contain_exactly(expected_highlights) - end - end - - it 'autocorrects to the right version' do - autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') - - expect(autocorrected).to eq(expected) - end - end - end -end diff --git a/spec/rubocop/cop/gem_fetcher_spec.rb b/spec/rubocop/cop/gem_fetcher_spec.rb deleted file mode 100644 index c07f6a831dc..00000000000 --- a/spec/rubocop/cop/gem_fetcher_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../rubocop/cop/gem_fetcher' - -describe RuboCop::Cop::GemFetcher do - include CopHelper - - subject(:cop) { described_class.new } - - context 'in Gemfile' do - before do - allow(cop).to receive(:gemfile?).and_return(true) - end - - it 'registers an offense when a gem uses `git`' do - inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - expect(cop.highlights).to eq(['git: "https://gitlab.com/foo/bar.git"']) - end - end - - it 'registers an offense when a gem uses `github`' do - inspect_source(cop, 'gem "foo", github: "foo/bar.git"') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - expect(cop.highlights).to eq(['github: "foo/bar.git"']) - end - end - end - - context 'outside of Gemfile' do - it 'registers no offense' do - inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"') - - expect(cop.offenses.size).to eq(0) - end - end -end diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb index 1fd40653f79..8e2d5f70353 100644 --- a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb +++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb @@ -12,7 +12,7 @@ describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do let(:offending_lines) { options[:offending_lines] } it 'registers an offense when instance variable is used in a module' do - inspect_source(cop, source) + inspect_source(source) aggregate_failures do expect(cop.offenses.size).to eq(offending_lines.size) @@ -23,7 +23,7 @@ describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do shared_examples('not registering offense') do it 'does not register offenses' do - inspect_source(cop, source) + inspect_source(source) expect(cop.offenses).to be_empty end diff --git a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb new file mode 100644 index 00000000000..21fc4584654 --- /dev/null +++ b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/predicate_memoization' + +describe RuboCop::Cop::Gitlab::PredicateMemoization do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples('registering offense') do |options| + let(:offending_lines) { options[:offending_lines] } + + it 'registers an offense when a predicate method is memoizing via ivar' do + inspect_source(source) + + aggregate_failures do + expect(cop.offenses.size).to eq(offending_lines.size) + expect(cop.offenses.map(&:line)).to eq(offending_lines) + end + end + end + + shared_examples('not registering offense') do + it 'does not register offenses' do + inspect_source(source) + + expect(cop.offenses).to be_empty + end + end + + context 'when source is a predicate method memoizing via ivar' do + it_behaves_like 'registering offense', offending_lines: [3] do + let(:source) do + <<~RUBY + class C + def really? + @really ||= true + end + end + RUBY + end + end + + it_behaves_like 'registering offense', offending_lines: [4] do + let(:source) do + <<~RUBY + class C + def really? + value = true + @really ||= value + end + end + RUBY + end + end + end + + context 'when source is a predicate method using ivar with assignment' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + class C + def really? + @really = true + end + end + RUBY + end + end + end + + context 'when source is a predicate method using local with ||=' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + class C + def really? + really ||= true + end + end + RUBY + end + end + end + + context 'when source is a regular method memoizing via ivar' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + class C + def really + @really ||= true + end + end + RUBY + end + end + end +end diff --git a/spec/rubocop/cop/in_batches_spec.rb b/spec/rubocop/cop/in_batches_spec.rb deleted file mode 100644 index 072481984c6..00000000000 --- a/spec/rubocop/cop/in_batches_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../rubocop/cop/in_batches' - -describe RuboCop::Cop::InBatches do - include CopHelper - - subject(:cop) { described_class.new } - - it 'registers an offense when in_batches is used' do - inspect_source(cop, 'foo.in_batches do; end') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end -end diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb index 7f406535dda..f5109287876 100644 --- a/spec/rubocop/cop/include_sidekiq_worker_spec.rb +++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' + require 'rubocop' require 'rubocop/rspec/support' + require_relative '../../../rubocop/cop/include_sidekiq_worker' describe RuboCop::Cop::IncludeSidekiqWorker do @@ -13,7 +15,7 @@ describe RuboCop::Cop::IncludeSidekiqWorker do let(:correct_source) { 'include ApplicationWorker' } it 'registers an offense ' do - inspect_source(cop, source) + inspect_source(source) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -23,7 +25,7 @@ describe RuboCop::Cop::IncludeSidekiqWorker do end it 'autocorrects to the right version' do - autocorrected = autocorrect_source(cop, source) + autocorrected = autocorrect_source(source) expect(autocorrected).to eq(correct_source) end diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb deleted file mode 100644 index 8899dc85384..00000000000 --- a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb +++ /dev/null @@ -1,160 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../rubocop/cop/line_break_after_guard_clauses' - -describe RuboCop::Cop::LineBreakAfterGuardClauses do - include CopHelper - - subject(:cop) { described_class.new } - - shared_examples 'examples with guard clause' do |title| - %w[if unless].each do |conditional| - it "flags violation for #{title} #{conditional} without line breaks" do - source = <<~RUBY - #{title} #{conditional} condition - do_stuff - RUBY - inspect_source(cop, source) - - expect(cop.offenses.size).to eq(1) - offense = cop.offenses.first - - expect(offense.line).to eq(1) - expect(cop.highlights).to eq(["#{title} #{conditional} condition"]) - expect(offense.message).to eq('Add a line break after guard clauses') - end - - it "doesn't flag violation for #{title} #{conditional} with line break" do - source = <<~RUBY - #{title} #{conditional} condition - - do_stuff - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do - source = <<~RUBY - #{conditional} condition - #{title} - end - do_stuff - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do - source = <<~RUBY - def test - #{title} #{conditional} condition - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do - source = <<~RUBY - if model - #{title} #{conditional} condition - elsif - do_something - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do - source = <<~RUBY - if model - #{title} #{conditional} condition - else - do_something - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do - source = <<~RUBY - case model - when condition_a - #{title} #{conditional} condition - when condition_b - do_something - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do - source = <<~RUBY - begin - #{title} #{conditional} condition - rescue StandardError - do_something - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do - source = <<~RUBY - def foo - #{title} #{conditional} condition - ensure - do_something - end - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do - source = <<~RUBY - #{title} #{conditional} condition - #{title} #{conditional} condition - - do_stuff - RUBY - inspect_source(cop, source) - - expect(cop.offenses).to be_empty - end - - it "autocorrects #{title} #{conditional} guard clauses without line break" do - source = <<~RUBY - #{title} #{conditional} condition - do_stuff - RUBY - autocorrected = autocorrect_source(cop, source) - - expected_source = <<~RUBY - #{title} #{conditional} condition - - do_stuff - RUBY - expect(autocorrected).to eql(expected_source) - end - end - end - - %w[return fail raise next break throw].each do |example| - it_behaves_like 'examples with guard clause', example - end -end diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb new file mode 100644 index 00000000000..7ddf9141fcd --- /dev/null +++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb @@ -0,0 +1,411 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/line_break_around_conditional_block' + +describe RuboCop::Cop::LineBreakAroundConditionalBlock do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples 'examples with conditional' do |conditional| + it "flags violation for #{conditional} without line break before" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(2) + expect(cop.highlights).to eq(["#{conditional} condition\n do_something_more\nend"]) + expect(offense.message).to eq('Add a line break around conditional blocks') + end + + it "flags violation for #{conditional} without line break after" do + source = <<~RUBY + #{conditional} condition + do_something + end + do_something_more + RUBY + inspect_source(source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(1) + expect(cop.highlights).to eq(["#{conditional} condition\n do_something\nend"]) + expect(offense.message).to eq('Add a line break around conditional blocks') + end + + it "doesn't flag violation for #{conditional} with line break before and after" do + source = <<~RUBY + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a method definition" do + source = <<~RUBY + def a_method + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a class definition" do + source = <<~RUBY + class Foo + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a module definition" do + source = <<~RUBY + module Foo + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a begin definition" do + source = <<~RUBY + begin + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an assign/begin definition" do + source = <<~RUBY + @project ||= begin + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a block definition" do + source = <<~RUBY + on_block(param_a) do |item| + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a block definition using brackets" do + source = <<~RUBY + on_block(param_a) { |item| + #{conditional} condition + do_something + end + } + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a comment" do + source = <<~RUBY + # a short comment + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an assignment" do + source = <<~RUBY + foo = + #{conditional} condition + do_something + else + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a multiline comment" do + source = <<~RUBY + =begin + a multiline comment + =end + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by another conditional" do + source = <<~RUBY + #{conditional} condition_a + #{conditional} condition_b + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an else" do + source = <<~RUBY + if condition_a + do_something + else + #{conditional} condition_b + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an elsif" do + source = <<~RUBY + if condition_a + do_something + elsif condition_b + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an ensure" do + source = <<~RUBY + def a_method + ensure + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a when" do + source = <<~RUBY + case field + when value + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an end" do + source = <<~RUBY + class Foo + + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an else" do + source = <<~RUBY + #{conditional} condition_a + #{conditional} condition_b + do_something + end + else + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by a when" do + source = <<~RUBY + case + when condition_a + #{conditional} condition_b + do_something + end + when condition_c + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an elsif" do + source = <<~RUBY + if condition_a + #{conditional} condition_b + do_something + end + elsif condition_c + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by a rescue" do + source = <<~RUBY + def a_method + #{conditional} condition + do_something + end + rescue + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "autocorrects #{conditional} without line break before" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + do_something + + #{conditional} condition + do_something_more + end + RUBY + expect(autocorrected).to eql(expected_source) + end + + it "autocorrects #{conditional} without line break after" do + source = <<~RUBY + #{conditional} condition + do_something + end + do_something_more + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + #{conditional} condition + do_something + end + + do_something_more + RUBY + expect(autocorrected).to eql(expected_source) + end + + it "autocorrects #{conditional} without line break before and after" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + do_something_extra + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + do_something + + #{conditional} condition + do_something_more + end + + do_something_extra + RUBY + expect(autocorrected).to eql(expected_source) + end + end + + %w[if unless].each do |example| + it_behaves_like 'examples with conditional', example + end + + it "doesn't flag violation for if with elsif" do + source = <<~RUBY + if condition + do_something + elsif another_condition + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end +end diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb index 7cb24dc5646..1df1fffb94e 100644 --- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb +++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' + require 'rubocop' require 'rubocop/rspec/support' + require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key' describe RuboCop::Cop::Migration::AddConcurrentForeignKey do @@ -10,7 +12,7 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do context 'outside of a migration' do it 'does not register any offenses' do - inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id); end') expect(cop.offenses).to be_empty end @@ -22,7 +24,7 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do end it 'registers an offense when using add_foreign_key' do - inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id); end') aggregate_failures do expect(cop.offenses.size).to eq(1) diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb index 19a5718b0b1..9c1ebcc0ced 100644 --- a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb +++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb @@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do end it 'registers an offense when add_concurrent_index is used inside a change method' do - inspect_source(cop, 'def change; add_concurrent_index :table, :column; end') + inspect_source('def change; add_concurrent_index :table, :column; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do end it 'registers no offense when add_concurrent_index is used inside an up method' do - inspect_source(cop, 'def up; add_concurrent_index :table, :column; end') + inspect_source('def up; add_concurrent_index :table, :column; end') expect(cop.offenses.size).to eq(0) end @@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, 'def change; add_concurrent_index :table, :column; end') + inspect_source('def change; add_concurrent_index :table, :column; end') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb index 18df62dec3e..3a41c91add2 100644 --- a/spec/rubocop/cop/migration/add_timestamps_spec.rb +++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb @@ -53,7 +53,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do end it 'registers an offense when the "add_timestamps" method is used' do - inspect_source(cop, migration_with_add_timestamps) + inspect_source(migration_with_add_timestamps) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -62,7 +62,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do end it 'does not register an offense when the "add_timestamps" method is not used' do - inspect_source(cop, migration_without_add_timestamps) + inspect_source(migration_without_add_timestamps) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -70,7 +70,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do end it 'does not register an offense when the "add_timestamps_with_timezone" method is used' do - inspect_source(cop, migration_with_add_timestamps_with_timezone) + inspect_source(migration_with_add_timestamps_with_timezone) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -80,9 +80,9 @@ describe RuboCop::Cop::Migration::AddTimestamps do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, migration_with_add_timestamps) - inspect_source(cop, migration_without_add_timestamps) - inspect_source(cop, migration_with_add_timestamps_with_timezone) + inspect_source(migration_with_add_timestamps) + inspect_source(migration_without_add_timestamps) + inspect_source(migration_with_add_timestamps_with_timezone) expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb index b1dfcf1b048..9e844325371 100644 --- a/spec/rubocop/cop/migration/datetime_spec.rb +++ b/spec/rubocop/cop/migration/datetime_spec.rb @@ -67,7 +67,7 @@ describe RuboCop::Cop::Migration::Datetime do end it 'registers an offense when the ":datetime" data type is used' do - inspect_source(cop, migration_with_datetime) + inspect_source(migration_with_datetime) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -77,7 +77,7 @@ describe RuboCop::Cop::Migration::Datetime do end it 'registers an offense when the ":timestamp" data type is used' do - inspect_source(cop, migration_with_timestamp) + inspect_source(migration_with_timestamp) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -87,7 +87,7 @@ describe RuboCop::Cop::Migration::Datetime do end it 'does not register an offense when the ":datetime" data type is not used' do - inspect_source(cop, migration_without_datetime) + inspect_source(migration_without_datetime) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -95,7 +95,7 @@ describe RuboCop::Cop::Migration::Datetime do end it 'does not register an offense when the ":datetime_with_timezone" data type is used' do - inspect_source(cop, migration_with_datetime_with_timezone) + inspect_source(migration_with_datetime_with_timezone) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -105,10 +105,10 @@ describe RuboCop::Cop::Migration::Datetime do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, migration_with_datetime) - inspect_source(cop, migration_with_timestamp) - inspect_source(cop, migration_without_datetime) - inspect_source(cop, migration_with_datetime_with_timezone) + inspect_source(migration_with_datetime) + inspect_source(migration_with_timestamp) + inspect_source(migration_without_datetime) + inspect_source(migration_with_datetime_with_timezone) expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/hash_index_spec.rb b/spec/rubocop/cop/migration/hash_index_spec.rb index 9a8576a19e5..5d53dde9a79 100644 --- a/spec/rubocop/cop/migration/hash_index_spec.rb +++ b/spec/rubocop/cop/migration/hash_index_spec.rb @@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::HashIndex do end it 'registers an offense when creating a hash index' do - inspect_source(cop, 'def change; add_index :table, :column, using: :hash; end') + inspect_source('def change; add_index :table, :column, using: :hash; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::HashIndex do end it 'registers an offense when creating a concurrent hash index' do - inspect_source(cop, 'def change; add_concurrent_index :table, :column, using: :hash; end') + inspect_source('def change; add_concurrent_index :table, :column, using: :hash; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -34,7 +34,7 @@ describe RuboCop::Cop::Migration::HashIndex do end it 'registers an offense when creating a hash index using t.index' do - inspect_source(cop, 'def change; t.index :table, :column, using: :hash; end') + inspect_source('def change; t.index :table, :column, using: :hash; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -45,7 +45,7 @@ describe RuboCop::Cop::Migration::HashIndex do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, 'def change; index :table, :column, using: :hash; end') + inspect_source('def change; index :table, :column, using: :hash; end') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb index 89112f01723..f1a64f431bd 100644 --- a/spec/rubocop/cop/migration/remove_column_spec.rb +++ b/spec/rubocop/cop/migration/remove_column_spec.rb @@ -21,7 +21,7 @@ describe RuboCop::Cop::Migration::RemoveColumn do end it 'registers an offense when remove_column is used in the change method' do - inspect_source(cop, source('change')) + inspect_source(source('change')) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -30,7 +30,7 @@ describe RuboCop::Cop::Migration::RemoveColumn do end it 'registers an offense when remove_column is used in the up method' do - inspect_source(cop, source('up')) + inspect_source(source('up')) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -39,7 +39,7 @@ describe RuboCop::Cop::Migration::RemoveColumn do end it 'registers no offense when remove_column is used in the down method' do - inspect_source(cop, source('down')) + inspect_source(source('down')) expect(cop.offenses.size).to eq(0) end @@ -52,7 +52,7 @@ describe RuboCop::Cop::Migration::RemoveColumn do end it 'registers no offense' do - inspect_source(cop, source) + inspect_source(source) expect(cop.offenses.size).to eq(0) end @@ -60,7 +60,7 @@ describe RuboCop::Cop::Migration::RemoveColumn do context 'outside of a migration' do it 'registers no offense' do - inspect_source(cop, source) + inspect_source(source) expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb index a714bf4e5d5..a23d5d022e3 100644 --- a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb +++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb @@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do end it 'registers an offense when remove_concurrent_index is used inside a change method' do - inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end') + inspect_source('def change; remove_concurrent_index :table, :column; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do end it 'registers no offense when remove_concurrent_index is used inside an up method' do - inspect_source(cop, 'def up; remove_concurrent_index :table, :column; end') + inspect_source('def up; remove_concurrent_index :table, :column; end') expect(cop.offenses.size).to eq(0) end @@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end') + inspect_source('def change; remove_concurrent_index :table, :column; end') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb index 31923cb7429..bbf2227e512 100644 --- a/spec/rubocop/cop/migration/remove_index_spec.rb +++ b/spec/rubocop/cop/migration/remove_index_spec.rb @@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::RemoveIndex do end it 'registers an offense when remove_index is used' do - inspect_source(cop, 'def change; remove_index :table, :column; end') + inspect_source('def change; remove_index :table, :column; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -27,7 +27,7 @@ describe RuboCop::Cop::Migration::RemoveIndex do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, 'def change; remove_index :table, :column; end') + inspect_source('def change; remove_index :table, :column; end') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb index 3723d635083..ba8cd2c6c4a 100644 --- a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb +++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb @@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do end it 'registers an offense when add_column_with_default is used inside a change method' do - inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end') + inspect_source('def change; add_column_with_default :table, :column, default: false; end') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do end it 'registers no offense when add_column_with_default is used inside an up method' do - inspect_source(cop, 'def up; add_column_with_default :table, :column, default: false; end') + inspect_source('def up; add_column_with_default :table, :column, default: false; end') expect(cop.offenses.size).to eq(0) end @@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end') + inspect_source('def change; add_column_with_default :table, :column, default: false; end') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb index 1006594499a..1c4f18fbcc3 100644 --- a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb +++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb @@ -32,7 +32,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do sources_and_offense.each do |source, offense| context "given the source \"#{source}\"" do it "registers the offense matching \"#{offense}\"" do - inspect_source(cop, source) + inspect_source(source) aggregate_failures do expect(cop.offenses.first.message).to match(offense) @@ -49,7 +49,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do inoffensive_sources.each do |source| context "given the source \"#{source}\"" do it "registers no offense" do - inspect_source(cop, source) + inspect_source(source) aggregate_failures do expect(cop.offenses).to be_empty @@ -61,14 +61,14 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do end it 'registers no offense for tables not listed in SMALL_TABLES' do - inspect_source(cop, "add_column :large_table, :column, :boolean") + inspect_source("add_column :large_table, :column, :boolean") expect(cop.offenses).to be_empty end it 'registers no offense for non-boolean columns' do table = described_class::SMALL_TABLES.sample - inspect_source(cop, "add_column :#{table}, :column, :string") + inspect_source("add_column :#{table}, :column, :string") expect(cop.offenses).to be_empty end @@ -77,7 +77,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do context 'outside of migration' do it 'registers no offense' do table = described_class::SMALL_TABLES.sample - inspect_source(cop, "add_column :#{table}, :column, :boolean") + inspect_source("add_column :#{table}, :column, :boolean") expect(cop.offenses).to be_empty end diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb index cdf1423d0c5..685bdb21803 100644 --- a/spec/rubocop/cop/migration/timestamps_spec.rb +++ b/spec/rubocop/cop/migration/timestamps_spec.rb @@ -62,7 +62,7 @@ describe RuboCop::Cop::Migration::Timestamps do end it 'registers an offense when the "timestamps" method is used' do - inspect_source(cop, migration_with_timestamps) + inspect_source(migration_with_timestamps) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -71,7 +71,7 @@ describe RuboCop::Cop::Migration::Timestamps do end it 'does not register an offense when the "timestamps" method is not used' do - inspect_source(cop, migration_without_timestamps) + inspect_source(migration_without_timestamps) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -79,7 +79,7 @@ describe RuboCop::Cop::Migration::Timestamps do end it 'does not register an offense when the "timestamps_with_timezone" method is used' do - inspect_source(cop, migration_with_timestamps_with_timezone) + inspect_source(migration_with_timestamps_with_timezone) aggregate_failures do expect(cop.offenses.size).to eq(0) @@ -89,9 +89,9 @@ describe RuboCop::Cop::Migration::Timestamps do context 'outside of migration' do it 'registers no offense' do - inspect_source(cop, migration_with_timestamps) - inspect_source(cop, migration_without_timestamps) - inspect_source(cop, migration_with_timestamps_with_timezone) + inspect_source(migration_with_timestamps) + inspect_source(migration_without_timestamps) + inspect_source(migration_with_timestamps_with_timezone) expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb index 38b8f439a55..1c8ab0ad5d2 100644 --- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' + require 'rubocop' require 'rubocop/rspec/support' + require_relative '../../../../rubocop/cop/migration/update_column_in_batches' describe RuboCop::Cop::Migration::UpdateColumnInBatches do @@ -25,7 +27,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do context 'outside of a migration' do it 'does not register any offenses' do - inspect_source(cop, migration_code) + inspect_source(migration_code) expect(cop.offenses).to be_empty end @@ -49,7 +51,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do let(:relative_spec_filepath) { Pathname.new(spec_filepath).relative_path_from(tmp_rails_root) } it 'registers an offense when using update_column_in_batches' do - inspect_source(cop, migration_code, @migration_file) + inspect_source(migration_code, @migration_file) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -72,7 +74,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do end it 'does not register any offenses' do - inspect_source(cop, migration_code, @migration_file) + inspect_source(migration_code, @migration_file) expect(cop.offenses).to be_empty end diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb index 17b19e139e4..ef724fc8bad 100644 --- a/spec/rubocop/cop/migration/update_large_table_spec.rb +++ b/spec/rubocop/cop/migration/update_large_table_spec.rb @@ -18,7 +18,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do shared_examples 'large tables' do |update_method| described_class::LARGE_TABLES.each do |table| it "registers an offense for the #{table} table" do - inspect_source(cop, "#{update_method} :#{table}, :column, default: true") + inspect_source("#{update_method} :#{table}, :column, default: true") aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -37,7 +37,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do end it 'registers no offense for non-blacklisted tables' do - inspect_source(cop, "add_column_with_default :table, :column, default: true") + inspect_source("add_column_with_default :table, :column, default: true") expect(cop.offenses).to be_empty end @@ -45,7 +45,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do it 'registers no offense for non-blacklisted methods' do table = described_class::LARGE_TABLES.sample - inspect_source(cop, "some_other_method :#{table}, :column, default: true") + inspect_source("some_other_method :#{table}, :column, default: true") expect(cop.offenses).to be_empty end @@ -55,13 +55,13 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do let(:table) { described_class::LARGE_TABLES.sample } it 'registers no offense for add_column_with_default' do - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + inspect_source("add_column_with_default :#{table}, :column, default: true") expect(cop.offenses).to be_empty end it 'registers no offense for update_column_in_batches' do - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + inspect_source("add_column_with_default :#{table}, :column, default: true") expect(cop.offenses).to be_empty end diff --git a/spec/rubocop/cop/polymorphic_associations_spec.rb b/spec/rubocop/cop/polymorphic_associations_spec.rb deleted file mode 100644 index 49959aa6419..00000000000 --- a/spec/rubocop/cop/polymorphic_associations_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../rubocop/cop/polymorphic_associations' - -describe RuboCop::Cop::PolymorphicAssociations do - include CopHelper - - subject(:cop) { described_class.new } - - context 'inside the app/models directory' do - it 'registers an offense when polymorphic: true is used' do - allow(cop).to receive(:in_model?).and_return(true) - - inspect_source(cop, 'belongs_to :foo, polymorphic: true') - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end - end - - context 'outside the app/models directory' do - it 'does nothing' do - allow(cop).to receive(:in_model?).and_return(false) - - inspect_source(cop, 'belongs_to :foo, polymorphic: true') - - expect(cop.offenses).to be_empty - end - end -end diff --git a/spec/rubocop/cop/project_path_helper_spec.rb b/spec/rubocop/cop/project_path_helper_spec.rb index bc47b45cad7..84e6eb7d87f 100644 --- a/spec/rubocop/cop/project_path_helper_spec.rb +++ b/spec/rubocop/cop/project_path_helper_spec.rb @@ -15,7 +15,7 @@ describe RuboCop::Cop::ProjectPathHelper do let(:correct_source) { 'edit_project_issue_path(@issue.project, @issue)' } it 'registers an offense' do - inspect_source(cop, source) + inspect_source(source) aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -25,7 +25,7 @@ describe RuboCop::Cop::ProjectPathHelper do end it 'autocorrects to the right version' do - autocorrected = autocorrect_source(cop, source) + autocorrected = autocorrect_source(source) expect(autocorrected).to eq(correct_source) end @@ -33,7 +33,7 @@ describe RuboCop::Cop::ProjectPathHelper do context 'when using namespace_project with a different namespace' do it 'registers no offense' do - inspect_source(cop, 'edit_namespace_project_issue_path(namespace, project)') + inspect_source('edit_namespace_project_issue_path(namespace, project)') expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/redirect_with_status_spec.rb b/spec/rubocop/cop/redirect_with_status_spec.rb deleted file mode 100644 index 5ad63567f84..00000000000 --- a/spec/rubocop/cop/redirect_with_status_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../rubocop/cop/redirect_with_status' - -describe RuboCop::Cop::RedirectWithStatus do - include CopHelper - - subject(:cop) { described_class.new } - let(:controller_fixture_without_status) do - %q( - class UserController < ApplicationController - def show - user = User.find(params[:id]) - redirect_to user_path if user.name == 'John Wick' - end - - def destroy - user = User.find(params[:id]) - - if user.destroy - redirect_to root_path - else - render :show - end - end - end - ) - end - - let(:controller_fixture_with_status) do - %q( - class UserController < ApplicationController - def show - user = User.find(params[:id]) - redirect_to user_path if user.name == 'John Wick' - end - - def destroy - user = User.find(params[:id]) - - if user.destroy - redirect_to root_path, status: 302 - else - render :show - end - end - end - ) - end - - context 'in controller' do - before do - allow(cop).to receive(:in_controller?).and_return(true) - end - - it 'registers an offense when a "destroy" action uses "redirect_to" without "status"' do - inspect_source(cop, controller_fixture_without_status) - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([12]) # 'redirect_to' is located on 12th line in controller_fixture. - expect(cop.highlights).to eq(['redirect_to']) - end - end - - it 'does not register an offense when a "destroy" action uses "redirect_to" with "status"' do - inspect_source(cop, controller_fixture_with_status) - - aggregate_failures do - expect(cop.offenses.size).to eq(0) - end - end - end - - context 'outside of controller' do - it 'registers no offense' do - inspect_source(cop, controller_fixture_without_status) - inspect_source(cop, controller_fixture_with_status) - - expect(cop.offenses.size).to eq(0) - end - end -end diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb index 4e859b6f6fa..659633f6467 100644 --- a/spec/rubocop/cop/rspec/env_assignment_spec.rb +++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb @@ -17,7 +17,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do shared_examples 'an offensive ENV#[]= call' do |content| it "registers an offense for `#{content}`" do - inspect_source(cop, content, source_file) + inspect_source(content, source_file) expect(cop.offenses.size).to eq(1) expect(cop.offenses.map(&:line)).to eq([1]) @@ -27,7 +27,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do shared_examples 'an autocorrected ENV#[]= call' do |content, autocorrected_content| it "registers an offense for `#{content}` and autocorrects it to `#{autocorrected_content}`" do - autocorrected = autocorrect_source(cop, content, source_file) + autocorrected = autocorrect_source(content, source_file) expect(autocorrected).to eql(autocorrected_content) end @@ -51,7 +51,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do context 'outside of a spec file' do it "does not register an offense for `#{OFFENSE_CALL_SINGLE_QUOTES_KEY}` in a non-spec file" do - inspect_source(cop, OFFENSE_CALL_SINGLE_QUOTES_KEY) + inspect_source(OFFENSE_CALL_SINGLE_QUOTES_KEY) expect(cop.offenses.size).to eq(0) end diff --git a/spec/rubocop/cop/rspec/single_line_hook_spec.rb b/spec/rubocop/cop/rspec/single_line_hook_spec.rb deleted file mode 100644 index 6cf0831d3ad..00000000000 --- a/spec/rubocop/cop/rspec/single_line_hook_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../../rubocop/cop/rspec/single_line_hook' - -describe RuboCop::Cop::RSpec::SingleLineHook do - include CopHelper - - subject(:cop) { described_class.new } - - # Override `CopHelper#inspect_source` to always appear to be in a spec file, - # so that our RSpec-only cop actually runs - def inspect_source(*args) - super(*args, 'foo_spec.rb') - end - - it 'registers an offense for a single-line `before` block' do - inspect_source(cop, 'before { do_something }') - - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - expect(cop.highlights).to eq(['before { do_something }']) - end - - it 'registers an offense for a single-line `after` block' do - inspect_source(cop, 'after(:each) { undo_something }') - - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - expect(cop.highlights).to eq(['after(:each) { undo_something }']) - end - - it 'registers an offense for a single-line `around` block' do - inspect_source(cop, 'around { |ex| do_something_else }') - - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - expect(cop.highlights).to eq(['around { |ex| do_something_else }']) - end - - it 'ignores a multi-line `before` block' do - inspect_source(cop, ['before do', - ' do_something', - 'end']) - - expect(cop.offenses.size).to eq(0) - end - - it 'ignores a multi-line `after` block' do - inspect_source(cop, ['after(:each) do', - ' undo_something', - 'end']) - - expect(cop.offenses.size).to eq(0) - end - - it 'ignores a multi-line `around` block' do - inspect_source(cop, ['around do |ex|', - ' do_something_else', - 'end']) - - expect(cop.offenses.size).to eq(0) - end -end diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb deleted file mode 100644 index 278662d32ea..00000000000 --- a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' -require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata' - -describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do - include CopHelper - - subject(:cop) { described_class.new } - - let(:source_file) { 'foo_spec.rb' } - - # Override `CopHelper#inspect_source` to always appear to be in a spec file, - # so that our RSpec-only cop actually runs - def inspect_source(*args) - super(*args, source_file) - end - - shared_examples 'examples with include syntax' do |title| - it "flags violation for #{title} examples that uses verbose include syntax" do - inspect_source(cop, "#{title} 'Test', js: true do; end") - - expect(cop.offenses.size).to eq(1) - offense = cop.offenses.first - - expect(offense.line).to eq(1) - expect(cop.highlights).to eq(["#{title} 'Test', js: true"]) - expect(offense.message).to eq('Use `:js` instead of `js: true`.') - end - - it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do - inspect_source(cop, "#{title} 'Test', :js do; end") - - expect(cop.offenses).to be_empty - end - - it "doesn't flag violation for #{title} examples that uses flag: symbol" do - inspect_source(cop, "#{title} 'Test', flag: :symbol do; end") - - expect(cop.offenses).to be_empty - end - - it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do - autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file) - - expect(autocorrected).to eql("#{title} 'Test', :js do; end") - end - end - - %w(describe context feature example_group it specify example scenario its).each do |example| - it_behaves_like 'examples with include syntax', example - end -end diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb index a31de381631..7f237d5ffbb 100644 --- a/spec/rubocop/cop/sidekiq_options_queue_spec.rb +++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' + require 'rubocop' require 'rubocop/rspec/support' + require_relative '../../../rubocop/cop/sidekiq_options_queue' describe RuboCop::Cop::SidekiqOptionsQueue do @@ -9,7 +11,7 @@ describe RuboCop::Cop::SidekiqOptionsQueue do subject(:cop) { described_class.new } it 'registers an offense when `sidekiq_options` is used with the `queue` option' do - inspect_source(cop, 'sidekiq_options queue: "some_queue"') + inspect_source('sidekiq_options queue: "some_queue"') aggregate_failures do expect(cop.offenses.size).to eq(1) @@ -19,7 +21,7 @@ describe RuboCop::Cop::SidekiqOptionsQueue do end it 'does not register an offense when `sidekiq_options` is used with another option' do - inspect_source(cop, 'sidekiq_options retry: false') + inspect_source('sidekiq_options retry: false') expect(cop.offenses).to be_empty end diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index 87c7b2ad36e..b5a55b4ef6e 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ClusterApplicationEntity do describe '#as_json' do - let(:application) { build(:cluster_applications_helm) } + let(:application) { build(:clusters_applications_helm) } subject { described_class.new(application).as_json } it 'has name' do @@ -18,7 +18,7 @@ describe ClusterApplicationEntity do end context 'when application is errored' do - let(:application) { build(:cluster_applications_helm, :errored) } + let(:application) { build(:clusters_applications_helm, :errored) } it 'has corresponded data' do expect(subject[:status]).to eq(:errored) diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index d3aefa2c9eb..2bd8162d1b7 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -21,18 +21,21 @@ describe DeployKeyEntity do user_id: deploy_key.user_id, title: deploy_key.title, fingerprint: deploy_key.fingerprint, - can_push: deploy_key.can_push, destroyed_when_orphaned: true, almost_orphaned: false, created_at: deploy_key.created_at, updated_at: deploy_key.updated_at, can_edit: false, - projects: [ + deploy_keys_projects: [ { - id: project.id, - name: project.name, - full_path: project_path(project), - full_name: project.full_name + can_push: false, + project: + { + id: project.id, + name: project.name, + full_path: project_path(project), + full_name: project.full_name + } } ] } diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb deleted file mode 100644 index bb54597c967..00000000000 --- a/spec/serializers/event_entity_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe EventEntity do - subject { described_class.represent(create(:event)).as_json } - - it 'exposes author' do - expect(subject).to include(:author) - end - - it 'exposes core elements of event' do - expect(subject).to include(:updated_at) - end -end diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 452754d7a79..505a9eaac5a 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -22,6 +22,7 @@ describe GroupChildEntity do avatar_url name description + markdown_description visibility type can_edit @@ -60,9 +61,10 @@ describe GroupChildEntity do end describe 'for a group', :nested_groups do + let(:description) { 'Awesomeness' } let(:object) do create(:group, :nested, :with_avatar, - description: 'Awesomeness') + description: description) end before do @@ -96,6 +98,14 @@ describe GroupChildEntity do expect(json[:edit_path]).to eq(edit_group_path(object)) end + context 'emoji in description' do + let(:description) { ':smile:' } + + it 'has the correct markdown_description' do + expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="smiling face with open mouth and smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>') + end + end + it_behaves_like 'group child json' end end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index a5924a8589c..80a271ba7fb 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -35,6 +35,81 @@ describe MergeRequestWidgetEntity do end end + describe 'metrics' do + context 'when metrics record exists with merged data' do + before do + resource.mark_as_merged! + resource.metrics.update!(merged_by: user) + end + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from metrics record' do + expect(subject.dig(:metrics, :merged_by, :id)) + .to eq(resource.metrics.merged_by_id) + end + end + + context 'when metrics record exists with closed data' do + before do + resource.close! + resource.metrics.update!(latest_closed_by: user) + end + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from metrics record' do + expect(subject.dig(:metrics, :closed_by, :id)) + .to eq(resource.metrics.latest_closed_by_id) + end + end + + context 'when metrics does not exists' do + before do + resource.mark_as_merged! + resource.metrics.destroy! + resource.reload + end + + context 'when events exists' do + let!(:closed_event) { create(:event, :closed, project: project, target: resource) } + let!(:merge_event) { create(:event, :merged, project: project, target: resource) } + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from events record' do + expect(subject.dig(:metrics, :merged_by, :id)) + .to eq(merge_event.author_id) + + expect(subject.dig(:metrics, :closed_by, :id)) + .to eq(closed_event.author_id) + + expect(subject.dig(:metrics, :merged_at).to_s) + .to eq(merge_event.updated_at.to_s) + + expect(subject.dig(:metrics, :closed_at).to_s) + .to eq(closed_event.updated_at.to_s) + end + end + + context 'when events does not exists' do + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + end + end + end + it 'has email_patches_path' do expect(subject[:email_patches_path]) .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch") @@ -115,4 +190,20 @@ describe MergeRequestWidgetEntity do end end end + + describe 'when source project is deleted' do + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + + it 'returns a blank rebase_path' do + allow(merge_request).to receive(:should_be_rebased?).and_return(true) + fork_project.destroy + merge_request.reload + + entity = described_class.new(merge_request, request: request).as_json + + expect(entity[:rebase_path]).to be_nil + end + end end diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb index 1a56164dba4..f0179e35652 100644 --- a/spec/services/boards/issues/create_service_spec.rb +++ b/spec/services/boards/issues/create_service_spec.rb @@ -11,7 +11,7 @@ describe Boards::Issues::CreateService do subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') } before do - project.team << [user, :developer] + project.add_developer(user) end it 'delegates the create proceedings to Issues::CreateService' do diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 01ee3856c99..ff5733b7064 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -34,7 +34,7 @@ describe Boards::Issues::ListService do let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) } before do - project.team << [user, :developer] + project.add_developer(user) end it 'delegates search to IssuesFinder' do diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 464ff9f94b3..280e411683e 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -15,7 +15,7 @@ describe Boards::Issues::MoveService do let!(:closed) { create(:closed_list, board: board1) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when moving an issue between lists' do diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 7d0b396cd06..d5322e1bb21 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::CreateService do subject(:service) { described_class.new(project, user, label_id: label.id) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when board lists is empty' do diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb index 592f25059ac..82dbd1ee744 100644 --- a/spec/services/boards/lists/generate_service_spec.rb +++ b/spec/services/boards/lists/generate_service_spec.rb @@ -9,7 +9,7 @@ describe Boards::Lists::GenerateService do subject(:service) { described_class.new(project, user) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when board lists is empty' do diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb new file mode 100644 index 00000000000..3e68d906e71 --- /dev/null +++ b/spec/services/check_gcp_project_billing_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe CheckGcpProjectBillingService do + include GoogleApi::CloudPlatformHelpers + + let(:service) { described_class.new } + let(:project_id) { 'test-project-1234' } + + describe '#execute' do + before do + stub_cloud_platform_projects_list(project_id: project_id) + end + + subject { service.execute('bogustoken') } + + context 'google account has a billing enabled gcp project' do + before do + stub_cloud_platform_projects_get_billing_info(project_id, true) + end + + it { is_expected.to all(satisfy { |project| project.project_id == project_id }) } + end + + context 'google account does not have a billing enabled gcp project' do + before do + stub_cloud_platform_projects_get_billing_info(project_id, false) + end + + it { is_expected.to eq([]) } + end + end +end diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index e2a9ed27e87..3fc4e499b0c 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -15,7 +15,7 @@ describe Ci::StopEnvironmentsService do context 'when user has permission to stop environment' do before do - project.team << [user, :developer] + project.add_developer(user) end context 'when environment is associated with removed branch' do @@ -57,7 +57,7 @@ describe Ci::StopEnvironmentsService do context 'when user does not have permission to stop environment' do context 'when user has no access to manage deployments' do before do - project.team << [user, :guest] + project.add_guest(user) end it 'does not stop environment' do @@ -86,7 +86,7 @@ describe Ci::StopEnvironmentsService do context 'when user has permission to stop environments' do before do - project.team << [user, :master] + project.add_master(user) end it 'does not stop environment' do diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 75fc05d36e9..6894c1797b0 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Clusters::Applications::CheckInstallationProgressService do RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze - let(:application) { create(:cluster_applications_helm, :installing) } + let(:application) { create(:clusters_applications_helm, :installing) } let(:service) { described_class.new(application) } let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } let(:errors) { nil } @@ -33,7 +33,7 @@ describe Clusters::Applications::CheckInstallationProgressService do end context 'when timeouted' do - let(:application) { create(:cluster_applications_helm, :timeouted) } + let(:application) { create(:clusters_applications_helm, :timeouted) } it_behaves_like 'a terminated installation' diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index 054a49ffedf..ad175226e92 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -2,17 +2,19 @@ require 'spec_helper' describe Clusters::Applications::InstallService do describe '#execute' do - let(:application) { create(:cluster_applications_helm, :scheduled) } + let(:application) { create(:clusters_applications_helm, :scheduled) } + let!(:install_command) { application.install_command } let(:service) { described_class.new(application) } - let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) } + let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } before do + allow(service).to receive(:install_command).and_return(install_command) allow(service).to receive(:helm_api).and_return(helm_client) end context 'when there are no errors' do before do - expect(helm_client).to receive(:install).with(application.install_command) + expect(helm_client).to receive(:install).with(install_command) allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil) end @@ -33,7 +35,7 @@ describe Clusters::Applications::InstallService do context 'when k8s cluster communication fails' do before do error = KubeException.new(500, 'system failure', nil) - expect(helm_client).to receive(:install).with(application.install_command).and_raise(error) + expect(helm_client).to receive(:install).with(install_command).and_raise(error) end it 'make the application errored' do @@ -45,7 +47,7 @@ describe Clusters::Applications::InstallService do end context 'when application cannot be persisted' do - let(:application) { build(:cluster_applications_helm, :scheduled) } + let(:application) { build(:clusters_applications_helm, :scheduled) } it 'make the application errored' do expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid) diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb index 047a6e44dab..473dbcbb692 100644 --- a/spec/services/clusters/applications/schedule_installation_service_spec.rb +++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb @@ -34,7 +34,7 @@ describe Clusters::Applications::ScheduleInstallationService do end context 'when installation is already in progress' do - let(:application) { create(:cluster_applications_helm, :installing) } + let(:application) { create(:clusters_applications_helm, :installing) } let(:cluster) { application.cluster } it_behaves_like 'a failing service' diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 08267d6e6a0..b9bfbb11511 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -266,7 +266,7 @@ describe CreateDeploymentService do context "while updating the 'first_deployed_to_production_at' time" do before do - merge_request.mark_as_merged + merge_request.metrics.update!(merged_at: Time.now) end context "for merge requests merged before the current deploy" do diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb index 19855c9bee2..9c9fba030e7 100644 --- a/spec/services/delete_branch_service_spec.rb +++ b/spec/services/delete_branch_service_spec.rb @@ -9,7 +9,7 @@ describe DeleteBranchService do describe '#execute' do context 'when user has access to push to repository' do before do - project.team << [user, :developer] + project.add_developer(user) end it 'removes the branch' do diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb index ab8df7b74cd..3895a0b3aea 100644 --- a/spec/services/discussions/resolve_service_spec.rb +++ b/spec/services/discussions/resolve_service_spec.rb @@ -9,7 +9,7 @@ describe Discussions::ResolveService do let(:service) { described_class.new(discussion.noteable.project, user, merge_request: merge_request) } before do - project.team << [user, :master] + project.add_master(user) end it "doesn't resolve discussions the user can't resolve" do diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb index e9f8f0efe6b..ace5f293097 100644 --- a/spec/services/files/delete_service_spec.rb +++ b/spec/services/files/delete_service_spec.rb @@ -37,7 +37,7 @@ describe Files::DeleteService do end before do - project.team << [user, :master] + project.add_master(user) end describe "#execute" do diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb index 085a28d267f..b9971776b33 100644 --- a/spec/services/files/multi_service_spec.rb +++ b/spec/services/files/multi_service_spec.rb @@ -36,12 +36,12 @@ describe Files::MultiService do end before do - project.team << [user, :master] + project.add_master(user) end describe '#execute' do context 'with a valid action' do - it 'returns a hash with the :success status ' do + it 'returns a hash with the :success status' do results = subject.execute expect(results[:status]).to eq(:success) @@ -51,7 +51,7 @@ describe Files::MultiService do context 'with an invalid action' do let(:action) { 'rename' } - it 'returns a hash with the :error status ' do + it 'returns a hash with the :error status' do results = subject.execute expect(results[:status]).to eq(:error) diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 2b4f8cd42ee..16bfbdf3089 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -24,7 +24,7 @@ describe Files::UpdateService do end before do - project.team << [user, :master] + project.add_master(user) end describe "#execute" do @@ -72,13 +72,15 @@ describe Files::UpdateService do end end - context 'when target branch is different than source branch' do - let(:branch_name) { "#{project.default_branch}-new" } + context 'with gitaly disabled', :skip_gitaly_mock do + context 'when target branch is different than source branch' do + let(:branch_name) { "#{project.default_branch}-new" } - it 'fires hooks only once' do - expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original + it 'fires hooks only once' do + expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original - subject.execute + subject.execute + end end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index cc3d4e7da49..26fdf8d4b24 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -11,7 +11,7 @@ describe GitPushService, services: true do let(:ref) { 'refs/heads/master' } before do - project.team << [user, :master] + project.add_master(user) end describe 'Push branches' do @@ -266,8 +266,8 @@ describe GitPushService, services: true do let(:commit) { project.commit } before do - project.team << [commit_author, :developer] - project.team << [user, :developer] + project.add_developer(commit_author) + project.add_developer(user) allow(commit).to receive_messages( safe_message: "this commit \n mentions #{issue.to_reference}", @@ -323,8 +323,8 @@ describe GitPushService, services: true do let(:commit_time) { Time.now } before do - project.team << [commit_author, :developer] - project.team << [user, :developer] + project.add_developer(commit_author) + project.add_developer(user) allow(commit).to receive_messages( safe_message: "this commit \n mentions #{issue.to_reference}", @@ -376,7 +376,7 @@ describe GitPushService, services: true do allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) .and_return(closing_commit) - project.team << [commit_author, :master] + project.add_master(commit_author) end context "to default branches" do diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index 05695aa8188..33405d7a7ec 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -35,7 +35,7 @@ describe GitTagPushService do before do stub_ci_pipeline_to_return_yaml_file - project.team << [user, :developer] + project.add_developer(user) end it "creates a new pipeline" do diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index bdaab88d673..53c85f73cde 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -54,7 +54,7 @@ describe Issuable::BulkUpdateService do context 'when the new assignee ID is a valid user' do it 'succeeds' do new_assignee = create(:user) - project.team << [new_assignee, :developer] + project.add_developer(new_assignee) result = bulk_update(merge_request, assignee_id: new_assignee.id) @@ -64,7 +64,7 @@ describe Issuable::BulkUpdateService do it 'updates the assignee to the user ID passed' do assignee = create(:user) - project.team << [assignee, :developer] + project.add_developer(assignee) expect { bulk_update(merge_request, assignee_id: assignee.id) } .to change { merge_request.reload.assignee }.from(user).to(assignee) @@ -92,7 +92,7 @@ describe Issuable::BulkUpdateService do context 'when the new assignee ID is a valid user' do it 'succeeds' do new_assignee = create(:user) - project.team << [new_assignee, :developer] + project.add_developer(new_assignee) result = bulk_update(issue, assignee_ids: [new_assignee.id]) @@ -102,7 +102,7 @@ describe Issuable::BulkUpdateService do it 'updates the assignee to the user ID passed' do assignee = create(:user) - project.team << [assignee, :developer] + project.add_developer(assignee) expect { bulk_update(issue, assignee_ids: [assignee.id]) } .to change { issue.reload.assignees.first }.from(user).to(assignee) end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 03f76bd428d..248e7d5a389 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -5,7 +5,7 @@ describe Issues::BuildService do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'for a single discussion' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 5c27e8fd561..47c1ebbeb81 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -4,14 +4,14 @@ describe Issues::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:issue) { create(:issue, assignees: [user2]) } + let(:issue) { create(:issue, assignees: [user2], author: create(:user)) } let(:project) { issue.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [guest, :guest] + project.add_master(user) + project.add_developer(user2) + project.add_guest(guest) end describe '#execute' do diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index d86da244520..79bcdc41fb0 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -13,8 +13,8 @@ describe Issues::CreateService do let(:labels) { create_pair(:label, project: project) } before do - project.team << [user, :master] - project.team << [assignee, :master] + project.add_master(user) + project.add_master(assignee) end let(:opts) do @@ -43,7 +43,7 @@ describe Issues::CreateService do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) end it 'filters out params that cannot be set without the :admin_issue permission' do @@ -130,7 +130,7 @@ describe Issues::CreateService do end it 'invalidates open issues counter for assignees when issue is assigned' do - project.team << [assignee, :master] + project.add_master(assignee) described_class.new(project, user, opts).execute @@ -160,7 +160,7 @@ describe Issues::CreateService do context 'issue create service' do context 'assignees' do before do - project.team << [user, :master] + project.add_master(user) end it 'removes assignee when user id is invalid' do @@ -180,7 +180,7 @@ describe Issues::CreateService do end it 'saves assignee when user id is valid' do - project.team << [assignee, :master] + project.add_master(assignee) opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } issue = described_class.new(project, user, opts).execute @@ -224,8 +224,8 @@ describe Issues::CreateService do end before do - project.team << [user, :master] - project.team << [assignee, :master] + project.add_master(user) + project.add_master(assignee) end it 'assigns and sets milestone to issuable from command' do @@ -242,7 +242,7 @@ describe Issues::CreateService do let(:project) { merge_request.source_project } before do - project.team << [user, :master] + project.add_master(user) end describe 'for a single discussion' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index f2b35a8fadf..322c91065e7 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -6,7 +6,7 @@ describe Issues::MoveService do let(:title) { 'Some issue' } let(:description) { 'Some issue description' } let(:old_project) { create(:project) } - let(:new_project) { create(:project) } + let(:new_project) { create(:project, group: create(:group)) } let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') } let(:old_issue) do @@ -20,8 +20,8 @@ describe Issues::MoveService do shared_context 'user can move issue' do before do - old_project.team << [user, :reporter] - new_project.team << [user, :reporter] + old_project.add_reporter(user) + new_project.add_reporter(user) labels = Array.new(2) { |x| "label%d" % (x + 1) } @@ -179,13 +179,15 @@ describe Issues::MoveService do { system: true, note: 'Some system note' }, { system: false, note: 'Some comment 2' }] end - + let(:award_names) { %w(thumbsup thumbsdown facepalm) } let(:notes_contents) { notes_params.map { |n| n[:note] } } before do note_params = { noteable: old_issue, project: old_project, author: author } - notes_params.each do |note| - create(:note, note_params.merge(note)) + notes_params.each_with_index do |note, index| + new_note = create(:note, note_params.merge(note)) + award_emoji_params = { awardable: new_note, name: award_names[index] } + create(:award_emoji, award_emoji_params) end end @@ -199,6 +201,10 @@ describe Issues::MoveService do expect(all_notes.pluck(:note).first(3)).to eq notes_contents end + it 'creates new emojis for the new notes' do + expect(all_notes.map(&:award_emoji).to_a.flatten.map(&:name)).to eq award_names + end + it 'adds a system note about move after rewritten notes' do expect(system_notes.last.note).to match /^moved from/ end @@ -244,7 +250,7 @@ describe Issues::MoveService do context 'issue description with uploads' do let(:uploader) { build(:file_uploader, project: old_project) } - let(:description) { "Text and #{uploader.to_markdown}" } + let(:description) { "Text and #{uploader.markdown_link}" } include_context 'issue move executed' @@ -289,6 +295,20 @@ describe Issues::MoveService do .to raise_error(StandardError, /Cannot move issue/) end end + + context 'project issue hooks' do + let!(:hook) { create(:project_hook, project: old_project, issues_events: true) } + + it 'executes project issue hooks' do + allow_any_instance_of(WebHookService).to receive(:execute) + + # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1, + # but since the entire spec run takes place in a transaction, we never + # actually get to the `after_commit` hook that queues these jobs. + expect { move_service.execute(old_issue, new_project) } + .not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError + end + end end describe 'move permissions' do @@ -301,7 +321,7 @@ describe Issues::MoveService do context 'user is reporter only in new project' do before do - new_project.team << [user, :reporter] + new_project.add_reporter(user) end it { expect { move }.to raise_error(StandardError, /permissions/) } @@ -309,7 +329,7 @@ describe Issues::MoveService do context 'user is reporter only in old project' do before do - old_project.team << [user, :reporter] + old_project.add_reporter(user) end it { expect { move }.to raise_error(StandardError, /permissions/) } @@ -317,8 +337,8 @@ describe Issues::MoveService do context 'user is reporter in one project and guest in another' do before do - new_project.team << [user, :guest] - old_project.team << [user, :reporter] + new_project.add_guest(user) + old_project.add_reporter(user) end it { expect { move }.to raise_error(StandardError, /permissions/) } @@ -346,8 +366,8 @@ describe Issues::MoveService do context 'movable issue with no assigned labels' do before do - old_project.team << [user, :reporter] - new_project.team << [user, :reporter] + old_project.add_reporter(user) + new_project.add_reporter(user) labels = Array.new(2) { |x| "label%d" % (x + 1) } diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 48fc98b3b2f..42e5d544f4c 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -8,7 +8,7 @@ describe Issues::ReopenService do context 'when user is not authorized to reopen issue' do before do guest = create(:user) - project.team << [guest, :guest] + project.add_guest(guest) perform_enqueued_jobs do described_class.new(project, guest).execute(issue) @@ -24,7 +24,7 @@ describe Issues::ReopenService do let(:user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) end it 'invalidates counter cache for assignees' do diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 67a86c50fc1..13accc6ae1b 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -14,7 +14,7 @@ describe Issues::ResolveDiscussions do let(:user) { create(:user) } before do - project.team << [user, :developer] + project.add_developer(user) end describe "for resolving discussions" do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index f07b81e842a..41237dd7160 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -13,13 +13,14 @@ describe Issues::UpdateService, :mailer do create(:issue, title: 'Old title', description: "for #{user2.to_reference}", assignee_ids: [user3.id], - project: project) + project: project, + author: create(:user)) end before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [user3, :developer] + project.add_master(user) + project.add_developer(user2) + project.add_developer(user3) end describe 'execute' do @@ -99,7 +100,7 @@ describe Issues::UpdateService, :mailer do context 'when current user cannot admin issues in the project' do let(:guest) { create(:user) } before do - project.team << [guest, :guest] + project.add_guest(guest) end it 'filters out params that cannot be set without the :admin_issue permission' do @@ -318,7 +319,7 @@ describe Issues::UpdateService, :mailer do let!(:subscriber) do create(:user).tap do |u| label.toggle_subscription(u, project) - project.team << [u, :developer] + project.add_developer(u) end end @@ -556,7 +557,7 @@ describe Issues::UpdateService, :mailer do context 'valid project' do before do - target_project.team << [user, :master] + target_project.add_master(user) end it 'calls the move service with the proper issue and project' do diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index a781fbc7f7d..78aa5d442e7 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -17,7 +17,7 @@ describe Labels::FindOrCreateService do let(:user) { create(:user) } subject(:service) { described_class.new(user, project, params) } before do - project.team << [user, :developer] + project.add_developer(user) end context 'when label does not exist at group level' do diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index 8809b282127..aa9aba6bdff 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -85,6 +85,19 @@ describe Labels::PromoteService do change(project_3.labels, :count).by(-1) end + it 'keeps users\' subscriptions' do + user2 = create(:user) + project_label_1_1.subscriptions.create(user: user, subscribed: true) + project_label_2_1.subscriptions.create(user: user, subscribed: true) + project_label_3_2.subscriptions.create(user: user, subscribed: true) + project_label_2_1.subscriptions.create(user: user2, subscribed: true) + + expect { service.execute(project_label_1_1) }.to change { Subscription.count }.from(4).to(3) + + expect(new_label.subscribed?(user)).to be_truthy + expect(new_label.subscribed?(user2)).to be_truthy + end + it 'recreates priorities' do service.execute(project_label_1_1) diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index 302c488d6c6..b3018169a1c 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -123,7 +123,7 @@ describe Members::ApproveAccessRequestService do context 'when current user can approve access request to the project' do before do - project.team << [user, :master] + project.add_master(user) group.add_owner(user) end diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb index d4ef31c0c74..757c45708b9 100644 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -13,7 +13,7 @@ describe Members::AuthorizedDestroyService do context 'Invited users' do # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504 it 'destroys invited project member' do - project.team << [member_user, :developer] + project.add_developer(member_user) member = create :project_member, :invited, project: project @@ -52,7 +52,7 @@ describe Members::AuthorizedDestroyService do context 'Project member' do it "unassigns issues and merge requests" do - project.team << [member_user, :developer] + project.add_developer(member_user) create :issue, project: project, assignees: [member_user] create :merge_request, target_project: project, source_project: project, assignee: member_user diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 2a793e0eb4d..6bd4718e780 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -6,7 +6,7 @@ describe Members::CreateService do let(:project_user) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) end it 'adds user to members' do diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 72f5e27180d..91152df3ad9 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -71,7 +71,7 @@ describe Members::DestroyService do context 'when a member is found' do before do - project.team << [member_user, :developer] + project.add_developer(member_user) group.add_developer(member_user) end let(:params) { { user_id: member_user.id } } @@ -88,7 +88,7 @@ describe Members::DestroyService do context 'when current user can destroy the given member' do before do - project.team << [user, :master] + project.add_master(user) group.add_owner(user) end diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index fcbe0e5985f..bda6383a346 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -8,7 +8,7 @@ describe MergeRequests::AssignIssuesService do let(:service) { described_class.new(project, user, merge_request: merge_request) } before do - project.team << [user, :developer] + project.add_developer(user) end it 'finds unassigned issues fixed in merge request' do diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index b5c92e681fb..e56d335a7d6 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -28,7 +28,7 @@ describe MergeRequests::BuildService do end before do - project.team << [user, :guest] + project.add_guest(user) end def stub_compare @@ -171,6 +171,45 @@ describe MergeRequests::BuildService do end end end + + context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do + let(:source_branch) { '12345-fix-issue' } + + before do + allow(project).to receive(:external_issue_tracker).and_return(false) + allow(project).to receive(:issues_enabled?).and_return(false) + end + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + end + + it 'uses the description of the commit as the description of the merge request' do + commit_description = commit_1.safe_message.split(/\n+/, 2).last + + expect(merge_request.description).to eq("#{commit_description}") + end + end + + context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do + let(:source_branch) { 'EXMPL-12345-fix-issue' } + + before do + allow(project).to receive(:external_issue_tracker).and_return(true) + allow(project).to receive(:issues_enabled?).and_return(false) + allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) + end + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + end + + it 'uses the description of the commit as the description of the merge request and appends the closes text' do + commit_description = commit_1.safe_message.split(/\n+/, 2).last + + expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345") + end + end end context 'more than one commit in the diff' do @@ -234,15 +273,46 @@ describe MergeRequests::BuildService do end end - context 'branch starts with external issue IID followed by a hyphen' do + context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do let(:source_branch) { '12345-fix-issue' } before do + allow(project).to receive(:external_issue_tracker).and_return(false) + allow(project).to receive(:issues_enabled?).and_return(false) + end + + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('12345 fix issue') + end + end + + context 'branch starts with JIRA-formatted external issue IID' do + let(:source_branch) { 'EXMPL-12345' } + + before do allow(project).to receive(:external_issue_tracker).and_return(true) + allow(project).to receive(:issues_enabled?).and_return(false) + allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) + end + + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('Resolve EXMPL-12345') + end + + it 'appends the closes text' do + expect(merge_request.description).to eq('Closes EXMPL-12345') end - it 'sets the title to: Resolves External Issue $issue-iid' do - expect(merge_request.title).to eq('Resolve External Issue 12345') + context 'followed by hyphenated text' do + let(:source_branch) { 'EXMPL-12345-fix-issue' } + + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"') + end + + it 'appends the closes text' do + expect(merge_request.description).to eq('Closes EXMPL-12345') + end end end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index b3886987316..216e0cd4266 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -4,14 +4,14 @@ describe MergeRequests::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2) } + let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) } let(:project) { merge_request.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [guest, :guest] + project.add_master(user) + project.add_developer(user2) + project.add_guest(guest) end describe '#execute' do @@ -52,6 +52,19 @@ describe MergeRequests::CloseService do end end + it 'updates metrics' do + metrics = merge_request.metrics + metrics_service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(metrics_service) + + expect(metrics_service).to receive(:close) + + described_class.new(project, user, {}).execute(merge_request) + end + it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do service = described_class.new(project, user, {}) diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index 0b32c51a16f..6cadcd438c3 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -32,14 +32,6 @@ describe MergeRequests::Conflicts::ListService do expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey end - it 'returns a falsey value when the MR has a missing ref after a force push' do - merge_request = create_merge_request('conflict-resolvable') - service = conflicts_service(merge_request) - allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError) - - expect(service.can_be_resolved_in_ui?).to be_falsey - end - it 'returns a falsey value when the MR does not support new diff notes' do merge_request = create_merge_request('conflict-resolvable') merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) @@ -76,5 +68,23 @@ describe MergeRequests::Conflicts::ListService do expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy end + + it 'returns a falsey value when the MR has a missing ref after a force push' do + merge_request = create_merge_request('conflict-resolvable') + service = conflicts_service(merge_request) + allow_any_instance_of(Gitlab::GitalyClient::ConflictsService).to receive(:list_conflict_files).and_raise(GRPC::Unknown) + + expect(service.can_be_resolved_in_ui?).to be_falsey + end + + context 'with gitaly disabled', :skip_gitaly_mock do + it 'returns a falsey value when the MR has a missing ref after a force push' do + merge_request = create_merge_request('conflict-resolvable') + service = conflicts_service(merge_request) + allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError) + + expect(service.can_be_resolved_in_ui?).to be_falsey + end + end end end diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index e28d8d7ae5c..cff09237005 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -111,15 +111,6 @@ describe MergeRequests::Conflicts::ResolveService do described_class.new(merge_request_from_fork).execute(user, params) end - it 'gets conflicts from the source project' do - # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't - # used in this case, but since the refactor, for simplification, - # we always use that repository for read only operations. - expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original - - subject - end - it 'creates a commit with the message' do subject @@ -132,6 +123,17 @@ describe MergeRequests::Conflicts::ResolveService do expect(merge_request_from_fork.source_branch_head.parents.map(&:id)) .to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head]) end + + context 'when gitaly is disabled', :skip_gitaly_mock do + it 'gets conflicts from the source project' do + # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't + # used in this case, but since the refactor, for simplification, + # we always use that repository for read only operations. + expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original + + subject + end + end end end diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 623b182b205..75553afc033 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -112,5 +112,24 @@ describe MergeRequests::CreateFromIssueService do expect(result[:merge_request].assignee).to eq(user) end + + context 'when ref branch is set' do + subject { described_class.new(project, user, issue_iid: issue.iid, ref: 'feature').execute } + + it 'sets the merge request source branch to the new issue branch' do + expect(subject[:merge_request].source_branch).to eq(issue.to_branch_name) + end + + it 'sets the merge request target branch to the ref branch' do + expect(subject[:merge_request].target_branch).to eq('feature') + end + + context 'when ref branch does not exist' do + it 'does not create a merge request' do + expect { described_class.new(project, user, issue_iid: issue.iid, ref: 'nobr').execute } + .not_to change { project.merge_requests.count } + end + end + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index a047f891ab2..5d226f34d2d 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -21,8 +21,8 @@ describe MergeRequests::CreateService do let(:merge_request) { service.execute } before do - project.team << [user, :master] - project.team << [assignee, :developer] + project.add_master(user) + project.add_developer(assignee) allow(service).to receive(:execute_hooks) end @@ -148,8 +148,8 @@ describe MergeRequests::CreateService do end before do - project.team << [user, :master] - project.team << [assignee, :master] + project.add_master(user) + project.add_master(assignee) end it 'assigns and sets milestone to issuable from command' do @@ -165,7 +165,7 @@ describe MergeRequests::CreateService do let(:assignee) { create(:user) } before do - project.team << [user, :master] + project.add_master(user) end it 'removes assignee_id when user id is invalid' do @@ -185,7 +185,7 @@ describe MergeRequests::CreateService do end it 'saves assignee when user id is valid' do - project.team << [assignee, :master] + project.add_master(assignee) opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } merge_request = described_class.new(project, user, opts).execute @@ -205,7 +205,7 @@ describe MergeRequests::CreateService do end it 'invalidates open merge request counter for assignees when merge request is assigned' do - project.team << [assignee, :master] + project.add_master(assignee) described_class.new(project, user, opts).execute @@ -249,8 +249,8 @@ describe MergeRequests::CreateService do end before do - project.team << [user, :master] - project.team << [assignee, :developer] + project.add_master(user) + project.add_developer(assignee) end it 'creates a `MergeRequestsClosingIssues` record for each issue' do @@ -263,5 +263,66 @@ describe MergeRequests::CreateService do expect(issue_ids).to match_array([first_issue.id, second_issue.id]) end end + + context 'when source and target projects are different' do + let(:target_project) { create(:project) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + target_project_id: target_project.id + } + end + + context 'when user can not access source project' do + before do + target_project.add_developer(assignee) + target_project.add_master(user) + end + + it 'raises an error' do + expect { described_class.new(project, user, opts).execute } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'when user can not access target project' do + before do + target_project.add_developer(assignee) + target_project.add_master(user) + end + + it 'raises an error' do + expect { described_class.new(project, user, opts).execute } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + + context 'when user sets source project id' do + let(:another_project) { create(:project) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + source_project_id: another_project.id + } + end + + before do + project.add_developer(assignee) + project.add_master(user) + end + + it 'ignores source_project_id' do + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.source_project_id).to eq(project.id) + end + end end end diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index aaabf3ed2b0..5ef6365fcc9 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -7,13 +7,14 @@ describe MergeRequests::FfMergeService do create(:merge_request, source_branch: 'flatten-dir', target_branch: 'improve/awesome', - assignee: user2) + assignee: user2, + author: create(:user)) end let(:project) { merge_request.project } before do - project.team << [user, :master] - project.team << [user2, :developer] + project.add_master(user) + project.add_developer(user2) end describe '#execute' do diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index f17db70faf6..240aa638f79 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -43,7 +43,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do it 'creates a system note' do note = merge_request.notes.last - expect(note.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{8}/ + expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}} end end diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index d2bd05d921f..70957431942 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::PostMergeService do let(:project) { merge_request.project } before do - project.team << [user, :master] + project.add_master(user) end describe '#execute' do @@ -22,5 +22,18 @@ describe MergeRequests::PostMergeService do expect { service.execute(merge_request) } .to change { project.open_merge_requests_count }.from(1).to(0) end + + it 'updates metrics' do + metrics = merge_request.metrics + metrics_service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(metrics_service) + + expect(metrics_service).to receive(:merge) + + described_class.new(project, user, {}).execute(merge_request) + end end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb new file mode 100644 index 00000000000..fc1c3d67203 --- /dev/null +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe MergeRequests::RebaseService do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:merge_request) do + create(:merge_request, + source_branch: 'feature_conflict', + target_branch: 'master') + end + let(:project) { merge_request.project } + let(:repository) { project.repository.raw } + + subject(:service) { described_class.new(project, user, {}) } + + before do + project.add_master(user) + end + + describe '#execute' do + context 'when another rebase is already in progress' do + before do + allow(merge_request).to receive(:rebase_in_progress?).and_return(true) + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: described_class::REBASE_ERROR) + end + end + + context 'when unexpected error occurs', :disable_gitaly do + before do + allow(repository).to receive(:run_git!).and_raise('Something went wrong') + end + + it 'saves a generic error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: described_class::REBASE_ERROR) + end + end + + context 'with git command failure', :disable_gitaly do + before do + allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong') + end + + it 'saves a generic error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: described_class::REBASE_ERROR) + end + end + + context 'valid params' do + shared_examples 'successful rebase' do + before do + service.execute(merge_request) + end + + it 'rebases source branch' do + parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha + target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + + it 'records the new SHA on the merge request' do + head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + expect(merge_request.reload.rebase_commit_sha).to eq(head_sha) + end + + it 'logs correct author and commiter' do + head_commit = merge_request.source_project.repository.commit(merge_request.source_branch) + + expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com') + expect(head_commit.author_name).to eq('Dmitriy Zaporozhets') + expect(head_commit.committer_email).to eq(user.email) + expect(head_commit.committer_name).to eq(user.name) + end + end + + context 'when Gitaly rebase feature is enabled' do + it_behaves_like 'successful rebase' + end + + context 'when Gitaly rebase feature is disabled', :disable_gitaly do + it_behaves_like 'successful rebase' + end + + context 'git commands', :disable_gitaly do + it 'sets GL_REPOSITORY env variable when calling git commands' do + expect(repository).to receive(:popen).exactly(3) + .with(anything, anything, hash_including('GL_REPOSITORY')) + .and_return(['', 0]) + + service.execute(merge_request) + end + end + + context 'fork' do + shared_examples 'successful fork rebase' do + let(:forked_project) do + fork_project(project, user, repository: true) + end + + let(:merge_request_from_fork) do + forked_project.repository.create_file( + user, + 'new-file-to-target', + '', + message: 'Add new file to target', + branch_name: 'master') + + create(:merge_request, + source_branch: 'master', source_project: forked_project, + target_branch: 'master', target_project: project) + end + + it 'rebases source branch' do + parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha + target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + end + + context 'when Gitaly rebase feature is enabled' do + it_behaves_like 'successful fork rebase' + end + + context 'when Gitaly rebase feature is disabled', :disable_gitaly do + it_behaves_like 'successful fork rebase' + end + end + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 61ec4709c59..903aa0a5078 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do before do allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs end it 'executes hooks with update action' do + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + expect(refresh_service).to have_received(:execute_hooks) .with(@merge_request, 'update', old_rev: @oldrev) @@ -72,6 +73,34 @@ describe MergeRequests::RefreshService do expect(@build_failed_todo).to be_done expect(@fork_build_failed_todo).to be_done end + + it 'reloads source branch MRs memoization' do + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }.to change { + refresh_service.instance_variable_get("@source_merge_requests").first.merge_request_diff + } + end + + context 'when source branch ref does not exists' do + before do + DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) + end + + it 'closes MRs without source branch ref' do + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') } + .to change { @merge_request.reload.state } + .from('opened') + .to('closed') + + expect(@fork_merge_request.reload).to be_open + end + + it 'does not change the merge request diff' do + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') } + .not_to change { @merge_request.reload.merge_request_diff } + end + end end context 'when pipeline exists for the source branch' do @@ -300,8 +329,8 @@ describe MergeRequests::RefreshService do let(:commit) { project.commit } before do - project.team << [commit_author, :developer] - project.team << [user, :developer] + project.add_developer(commit_author) + project.add_developer(user) allow(commit).to receive_messages( safe_message: "Closes #{issue.to_reference}", @@ -371,37 +400,21 @@ describe MergeRequests::RefreshService do end it 'references the commit that caused the Work in Progress status' do - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - allow(refresh_service).to receive(:find_new_commits) - refresh_service.instance_variable_set("@commits", [ - double( - id: 'aaaaaaa', - sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e', - short_id: 'aaaaaaa', - title: 'Fix issue', - work_in_progress?: false - ), - double( - id: 'bbbbbbb', - sha: '498214de67004b1da3d820901307bed2a68a8ef6', - short_id: 'bbbbbbb', - title: 'fixup! Fix issue', - work_in_progress?: true, - to_reference: 'bbbbbbb' - ), - double( - id: 'ccccccc', - sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141', - short_id: 'ccccccc', - title: 'fixup! Fix issue', - work_in_progress?: true, - to_reference: 'ccccccc' - ) - ]) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') - reload_mrs - expect(@merge_request.notes.last.note).to eq( - "marked as a **Work In Progress** from bbbbbbb" + wip_merge_request = create(:merge_request, + source_project: @project, + source_branch: 'wip', + target_branch: 'master', + target_project: @project) + + commits = wip_merge_request.commits + oldrev = commits.last.id + newrev = commits.first.id + wip_commit = wip_merge_request.commits.find(&:work_in_progress?) + + refresh_service.execute(oldrev, newrev, 'refs/heads/wip') + + expect(wip_merge_request.reload.notes.last.note).to eq( + "marked as a **Work In Progress** from #{wip_commit.id}" ) end diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index fa652611c6b..9ee37c51d95 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -4,13 +4,13 @@ describe MergeRequests::ReopenService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, :closed, assignee: user2) } + let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) } let(:project) { merge_request.project } before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [guest, :guest] + project.add_master(user) + project.add_developer(user2) + project.add_guest(guest) end describe '#execute' do @@ -47,6 +47,19 @@ describe MergeRequests::ReopenService do end end + it 'updates metrics' do + metrics = merge_request.metrics + service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(service) + + expect(service).to receive(:reopen) + + described_class.new(project, user, {}).execute(merge_request) + end + it 'refreshes the number of open merge requests for a valid MR' do service = described_class.new(project, user, {}) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 7a66b809550..c31259239ee 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -12,13 +12,14 @@ describe MergeRequests::UpdateService, :mailer do create(:merge_request, :simple, title: 'Old title', description: "FYI #{user2.to_reference}", assignee_id: user3.id, - source_project: project) + source_project: project, + author: create(:user)) end before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [user3, :developer] + project.add_master(user) + project.add_developer(user2) + project.add_developer(user3) end describe 'execute' do @@ -356,8 +357,8 @@ describe MergeRequests::UpdateService, :mailer do let!(:subscriber) { create(:user) { |u| label.toggle_subscription(u, project) } } before do - project.team << [non_subscriber, :developer] - project.team << [subscriber, :developer] + project.add_developer(non_subscriber) + project.add_developer(subscriber) end it 'sends notifications for subscribers of newly added labels' do diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 2bdf224804d..adad73f7e11 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -6,7 +6,7 @@ describe Milestones::CloseService do let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } before do - project.team << [user, :master] + project.add_master(user) end describe '#execute' do diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 8837b91051d..f2a18c7295a 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -7,7 +7,7 @@ describe Milestones::CreateService do describe '#execute' do context "valid params" do before do - project.team << [user, :master] + project.add_master(user) opts = { title: 'v2.1.9', diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index af35e17bfa7..9703780b0e9 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -8,7 +8,7 @@ describe Milestones::DestroyService do let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } before do - project.team << [user, :master] + project.add_master(user) end def service diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 661d26946e7..0ae26e87154 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -10,7 +10,7 @@ describe Notes::CreateService do describe '#execute' do before do - project.team << [user, :master] + project.add_master(user) end context "valid params" do diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index a2b3638059f..6ef5e93cb20 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -7,7 +7,7 @@ describe Notes::PostProcessService do describe '#execute' do before do - project.team << [user, :master] + project.add_master(user) note_opts = { note: 'Awesome comment', noteable_type: 'Issue', diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 0280a19098b..5eafe56c99d 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Notes::QuickActionsService do shared_context 'note on noteable' do let(:project) { create(:project) } - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:master) { create(:user).tap { |u| project.add_master(u) } } let(:assignee) { create(:user) } before do - project.team << [assignee, :master] + project.add_master(assignee) end end @@ -226,7 +226,7 @@ describe Notes::QuickActionsService do context 'CE restriction for issue assignees' do describe '/assign' do let(:project) { create(:project) } - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:master) { create(:user).tap { |u| project.add_master(u) } } let(:assignee) { create(:user) } let(:master) { create(:user) } let(:service) { described_class.new(project, master) } @@ -237,8 +237,8 @@ describe Notes::QuickActionsService do end before do - project.team << [master, :master] - project.team << [assignee, :master] + project.add_master(master) + project.add_master(assignee) end it 'adds only one assignee from the list' do diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb index 3210539f3ee..65b1d613998 100644 --- a/spec/services/notes/update_service_spec.rb +++ b/spec/services/notes/update_service_spec.rb @@ -9,9 +9,9 @@ describe Notes::UpdateService do let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") } before do - project.team << [user, :master] - project.team << [user2, :developer] - project.team << [user3, :developer] + project.add_master(user) + project.add_developer(user2) + project.add_developer(user3) end describe '#execute' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 43e2643f709..35eb84e5e88 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe NotificationService, :mailer do + include EmailSpec::Matchers + let(:notification) { described_class.new } let(:assignee) { create(:user) } @@ -31,6 +33,14 @@ describe NotificationService, :mailer do send_notifications(@u_disabled) should_not_email_anyone end + + it 'sends the proper notification reason header' do + send_notifications(@u_watcher) + should_only_email(@u_watcher) + email = find_email_for(@u_watcher) + + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::MENTIONED) + end end # Next shared examples are intended to test notifications of "participants" @@ -448,7 +458,7 @@ describe NotificationService, :mailer do context "merge request diff note" do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } before do @@ -459,11 +469,13 @@ describe NotificationService, :mailer do describe '#new_note' do it "records sent notifications" do - # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note + # 3 SentNotification are sent: the MR assignee and author, and the @u_watcher expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original notification.new_note(note) + expect(SentNotification.last(3).map(&:recipient).map(&:id)) + .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end end @@ -511,12 +523,35 @@ describe NotificationService, :mailer do should_not_email(issue.assignees.first) end + it 'properly prioritizes notification reason' do + # have assignee be both assigned and mentioned + issue.update_attribute(:description, "/cc #{assignee.to_reference} #{@u_mentioned.to_reference}") + + notification.new_issue(issue, @u_disabled) + + email = find_email_for(assignee) + expect(email).to have_header('X-GitLab-NotificationReason', 'assigned') + + email = find_email_for(@u_mentioned) + expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned') + end + + it 'adds "assigned" reason for assignees if any' do + notification.new_issue(issue, @u_disabled) + + email = find_email_for(assignee) + + expect(email).to have_header('X-GitLab-NotificationReason', 'assigned') + end + it "emails any mentioned users with the mention level" do issue.description = @u_mentioned.to_reference notification.new_issue(issue, @u_disabled) - should_email(@u_mentioned) + email = find_email_for(@u_mentioned) + expect(email).not_to be_nil + expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned') end it "emails the author if they've opted into notifications about their activity" do @@ -620,6 +655,14 @@ describe NotificationService, :mailer do should_not_email(@u_lazy_participant) end + it 'adds "assigned" reason for new assignee' do + notification.reassigned_issue(issue, @u_disabled, [assignee]) + + email = find_email_for(assignee) + + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + it 'emails previous assignee even if he has the "on mention" notif level' do issue.assignees = [@u_mentioned] notification.reassigned_issue(issue, @u_disabled, [@u_watcher]) @@ -910,6 +953,14 @@ describe NotificationService, :mailer do should_not_email(@u_lazy_participant) end + it 'adds "assigned" reason for assignee, if any' do + notification.new_merge_request(merge_request, @u_disabled) + + email = find_email_for(merge_request.assignee) + + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + it "emails any mentioned users with the mention level" do merge_request.description = @u_mentioned.to_reference @@ -924,6 +975,9 @@ describe NotificationService, :mailer do notification.new_merge_request(merge_request, merge_request.author) should_email(merge_request.author) + + email = find_email_for(merge_request.author) + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::OWN_ACTIVITY) end it "doesn't email the author if they haven't opted into notifications about their activity" do @@ -1009,6 +1063,14 @@ describe NotificationService, :mailer do should_not_email(@u_lazy_participant) end + it 'adds "assigned" reason for new assignee' do + notification.reassigned_merge_request(merge_request, merge_request.author) + + email = find_email_for(merge_request.assignee) + + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end + it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index 426593be428..f7ff8b80bd7 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -34,7 +34,7 @@ describe Projects::AutocompleteService do end it 'does not list project confidential issues for project members with guest role' do - project.team << [member, :guest] + project.add_guest(member) autocomplete = described_class.new(project, non_member) issues = autocomplete.issues.map(&:iid) @@ -66,7 +66,7 @@ describe Projects::AutocompleteService do end it 'lists project confidential issues for project members' do - project.team << [member, :developer] + project.add_developer(member) autocomplete = described_class.new(project, member) issues = autocomplete.issues.map(&:iid) @@ -93,26 +93,27 @@ describe Projects::AutocompleteService do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, group: group) } - let!(:group_milestone) { create(:milestone, group: group) } - let!(:project_milestone) { create(:milestone, project: project) } + let!(:group_milestone1) { create(:milestone, group: group, due_date: '2017-01-01', title: 'Second Title') } + let!(:group_milestone2) { create(:milestone, group: group, due_date: '2017-01-01', title: 'First Title') } + let!(:project_milestone) { create(:milestone, project: project, due_date: '2016-01-01') } let(:milestone_titles) { described_class.new(project, user).milestones.map(&:title) } - it 'includes project and group milestones' do - expect(milestone_titles).to eq([group_milestone.title, project_milestone.title]) + it 'includes project and group milestones and sorts them correctly' do + expect(milestone_titles).to eq([project_milestone.title, group_milestone2.title, group_milestone1.title]) end it 'does not include closed milestones' do - group_milestone.close + group_milestone1.close - expect(milestone_titles).to eq([project_milestone.title]) + expect(milestone_titles).to eq([project_milestone.title, group_milestone2.title]) end it 'does not include milestones from other projects in the group' do other_project = create(:project, group: group) project_milestone.update!(project: other_project) - expect(milestone_titles).to eq([group_milestone.title]) + expect(milestone_titles).to eq([group_milestone2.title, group_milestone1.title]) end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index dc89fdebce7..9a44dfde41b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -252,6 +252,12 @@ describe Projects::CreateService, '#execute' do end end + it 'writes project full path to .git/config' do + project = create_project(user, opts) + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 4057caca2ac..409d5de8d43 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -139,10 +139,10 @@ describe Projects::ForkService do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) end - it "creates fork with highest allowed level" do + it "creates fork with lowest level" do forked_project = fork_project(@from_project, @to_user) - expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end @@ -209,6 +209,19 @@ describe Projects::ForkService do expect(to_project.errors[:path]).to eq(['has already been taken']) end end + + context 'when the namespace has a lower visibility level than the project' do + it 'creates the project with the lower visibility level' do + public_project = create(:project, :public) + private_group = create(:group, :private) + group_owner = create(:user) + private_group.add_owner(group_owner) + + forked_project = fork_project(public_project, group_owner, namespace: private_group) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end end end diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb new file mode 100644 index 00000000000..bb0e274c93e --- /dev/null +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Projects::GitlabProjectsImportService do + set(:namespace) { build(:namespace) } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + subject { described_class.new(namespace.owner, { namespace_id: namespace.id, path: path, file: file }) } + + describe '#execute' do + context 'with an invalid path' do + let(:path) { '/invalid-path/' } + + it 'returns an invalid project' do + project = subject.execute + + expect(project).not_to be_persisted + expect(project).not_to be_valid + end + end + + context 'with a valid path' do + let(:path) { 'test-path' } + + it 'creates a project' do + project = subject.execute + + expect(project).to be_persisted + expect(project).to be_valid + end + end + end +end diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb index 50e59954f73..15699574b3a 100644 --- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb @@ -6,7 +6,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } - let!(:upload) { Upload.find_by(path: file_uploader.relative_path) } + let!(:upload) { Upload.find_by(path: file_uploader.upload_path) } let(:file_uploader) { build(:file_uploader, project: project) } let(:old_path) { File.join(base_path(legacy_storage), upload.path) } let(:new_path) { File.join(base_path(hashed_storage), upload.path) } @@ -58,6 +58,6 @@ describe Projects::HashedStorage::MigrateAttachmentsService do end def base_path(storage) - FileUploader.dynamic_path_builder(storage.disk_path) + File.join(FileUploader.root, storage.disk_path) end end diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index 3a3e47fd9c0..7b536cc05cb 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::HashedStorage::MigrateRepositoryService do let(:gitlab_shell) { Gitlab::Shell.new } - let(:project) { create(:project, :empty_repo, :wiki_repo) } + let(:project) { create(:project, :repository, :wiki_repo) } let(:service) { described_class.new(project) } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } @@ -33,6 +33,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do service.execute end + + it 'writes project full path to .git/config' do + service.execute + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + end end context 'when one move fails' do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2b1337bee7e..ef68742a463 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -54,6 +54,12 @@ describe Projects::TransferService do expect(project.disk_path).not_to eq(old_path) expect(project.disk_path).to start_with(group.path) end + + it 'updates project full path in .git/config' do + transfer_project(project, user, group) + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + end end context 'when transfer fails' do @@ -86,6 +92,12 @@ describe Projects::TransferService do expect(original_path).to eq current_path end + it 'rolls back project full path in .git/config' do + attempt_project_transfer + + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + end + it "doesn't send move notifications" do expect_any_instance_of(NotificationService).not_to receive(:project_was_moved) @@ -138,6 +150,7 @@ describe Projects::TransferService do before do group.add_owner(user) + unless gitlab_shell.add_repository(repository_storage, "#{group.full_path}/#{project.path}") raise 'failed to add repository' end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index d887f70efae..fc6aa713d6f 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -61,7 +61,7 @@ describe Projects::UpdateService do end end - context 'When project visibility is higher than parent group' do + context 'when project visibility is higher than parent group' do let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 835e83d6dba..53b3e5e365d 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -19,5 +19,21 @@ describe ProtectedBranches::CreateService do expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) end + + context 'when user does not have permission' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + end + + it 'creates a new protected branch if we skip authorization step' do + expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1) + end + + it 'raises Gitlab::Access:AccessDeniedError' do + expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index eb46480fa54..ae160d104f1 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -12,7 +12,7 @@ describe QuickActions::InterpretService do let(:service) { described_class.new(project, developer) } before do - project.team << [developer, :developer] + project.add_developer(developer) end describe '#execute' do @@ -440,7 +440,7 @@ describe QuickActions::InterpretService do let(:content) { "/assign @#{developer.username} @#{developer2.username}" } before do - project.team << [developer2, :developer] + project.add_developer(developer2) end context 'Issue' do diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb new file mode 100644 index 00000000000..de475d16586 --- /dev/null +++ b/spec/services/reset_project_cache_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ResetProjectCacheService do + let(:project) { create(:project) } + let(:user) { create(:user) } + + subject { described_class.new(project, user).execute } + + context 'when project cache_index is nil' do + before do + project.jobs_cache_index = nil + end + + it 'sets project cache_index to one' do + expect { subject }.to change { project.reload.jobs_cache_index }.from(nil).to(1) + end + end + + context 'when project cache_index is a numeric value' do + before do + project.update_attributes(jobs_cache_index: 1) + end + + it 'increments project cache index' do + expect { subject }.to change { project.reload.jobs_cache_index }.by(1) + end + end +end diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb index eae9bd4f5cf..bc7885b03d9 100644 --- a/spec/services/search/snippet_service_spec.rb +++ b/spec/services/search/snippet_service_spec.rb @@ -33,7 +33,7 @@ describe Search::SnippetService do it 'returns public, internal snippets and project private snippets for project members' do member = create(:user) - project.team << [member, :developer] + project.add_developer(member) search = described_class.new(member, search: 'password') results = search.execute diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 46cd10cdc12..c40cd5b7548 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -105,12 +105,25 @@ describe SystemHooksService do expect(data[:old_username]).to eq(user.username_was) end end + + context 'user_failed_login' do + it 'contains state of user' do + user.ldap_block! + + data = event_data(user, :failed_login) + + expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :state) + expect(data[:username]).to eq(user.username) + expect(data[:state]).to eq('ldap_blocked') + end + end end context 'event names' do it { expect(event_name(user, :create)).to eq "user_create" } it { expect(event_name(user, :destroy)).to eq "user_destroy" } it { expect(event_name(user, :rename)).to eq 'user_rename' } + it { expect(event_name(user, :failed_login)).to eq 'user_failed_login' } it { expect(event_name(project, :create)).to eq "project_create" } it { expect(event_name(project, :destroy)).to eq "project_destroy" } it { expect(event_name(project, :rename)).to eq "project_rename" } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9025589ae0b..5b5edc1aa0d 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe SystemNoteService do include Gitlab::Routing + include RepoHelpers set(:group) { create(:group) } set(:project) { create(:project, :repository, group: group) } @@ -53,10 +54,11 @@ describe SystemNoteService do expect(note_lines[0]).to eq "added #{new_commits.size} commits" end - it 'adds a message line for each commit' do - new_commits.each_with_index do |commit, i| - # Skip the header - expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}" + it 'adds a message for each commit' do + decoded_note_content = HTMLEntities.new.decode(subject.note) + + new_commits.each do |commit| + expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>") end end end @@ -68,7 +70,7 @@ describe SystemNoteService do let(:old_commits) { [noteable.commits.last] } it 'includes the existing commit' do - expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`" + expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>") end end @@ -78,22 +80,16 @@ describe SystemNoteService do context 'with oldrev' do let(:oldrev) { noteable.commits[2].id } - it 'includes a commit range' do - expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}" - end - - it 'includes a commit count' do - expect(summary_line).to end_with " - 26 commits from branch `feature`" + it 'includes a commit range and count' do + expect(summary_line) + .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>") end end context 'without oldrev' do - it 'includes a commit range' do - expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}" - end - - it 'includes a commit count' do - expect(summary_line).to end_with " - 26 commits from branch `feature`" + it 'includes a commit range and count' do + expect(summary_line) + .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>") end end @@ -103,7 +99,7 @@ describe SystemNoteService do end it 'includes the project namespace' do - expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`" + expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>") end end end @@ -157,7 +153,7 @@ describe SystemNoteService do end it 'builds a correct phrase when assignee removed' do - expect(build_note([assignee1], [])).to eq 'removed assignee' + expect(build_note([assignee1], [])).to eq "unassigned @#{assignee1.username}" end it 'builds a correct phrase when assignees changed' do @@ -307,7 +303,7 @@ describe SystemNoteService do end it "posts the 'merge when pipeline succeeds' system note" do - expect(subject.note).to match(/enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/) + expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds}) end end @@ -692,9 +688,9 @@ describe SystemNoteService do describe '.new_commit_summary' do it 'escapes HTML titles' do commit = double(title: '<pre>This is a test</pre>', short_id: '12345678') - escaped = '<pre>This is a test</pre>' + escaped = '<pre>This is a test</pre>' - expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}])) + expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/)) end end @@ -726,6 +722,7 @@ describe SystemNoteService do else "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/merge_requests/#{merge_request.iid}" end + link = double(object: { 'url' => url }) links << link expect(link).to receive(:save!) @@ -1070,17 +1067,32 @@ describe SystemNoteService do let(:action) { 'outdated' } end - it 'creates a new note in the discussion' do - # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. - expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + context 'when the change_position is valid for the discussion' do + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'links to the diff in the system note' do + expect(subject.note).to include('version 1') + + diff_id = merge_request.merge_request_diff.id + line_code = change_position.line_code(project.repository) + expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) + end end - it 'links to the diff in the system note' do - expect(subject.note).to include('version 1') + context 'when the change_position is invalid for the discussion' do + let(:change_position) { project.commit(sample_commit.id) } + + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end - diff_id = merge_request.merge_request_diff.id - line_code = change_position.line_code(project.repository) - expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) + it 'does not create a link' do + expect(subject.note).to eq('changed this line in version 1 of the diff') + end end end diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb index ff8b9595538..74d7715e50f 100644 --- a/spec/services/test_hooks/system_service_spec.rb +++ b/spec/services/test_hooks/system_service_spec.rb @@ -60,5 +60,25 @@ describe TestHooks::SystemService do expect(service.execute).to include(success_result) end end + + context 'merge_requests_events' do + let(:trigger) { 'merge_requests_events' } + + it 'returns error message if the user does not have any repository with a merge request' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure one of your projects has merge requests.' }) + end + + it 'executes hook' do + trigger_key = :merge_request_hooks + sample_data = { data: 'sample' } + create(:project_member, user: current_user, project: project) + create(:merge_request, source_project: project) + allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(service.execute).to include(success_result) + end + end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 88013acae0a..5e6c24f5730 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -17,11 +17,11 @@ describe TodoService do let(:service) { described_class.new } before do - project.team << [guest, :guest] - project.team << [author, :developer] - project.team << [member, :developer] - project.team << [john_doe, :developer] - project.team << [skipped, :developer] + project.add_guest(guest) + project.add_developer(author) + project.add_developer(member) + project.add_developer(john_doe) + project.add_developer(skipped) end describe 'Issues' do diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb new file mode 100644 index 00000000000..b5fb999381d --- /dev/null +++ b/spec/services/update_merge_request_metrics_service_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe MergeRequestMetricsService do + let(:metrics) { create(:merge_request).metrics } + + describe '#merge' do + it 'updates metrics' do + user = create(:user) + service = described_class.new(metrics) + event = double(Event, author_id: user.id, created_at: Time.now) + + service.merge(event) + + expect(metrics.merged_by).to eq(user) + expect(metrics.merged_at).to eq(event.created_at) + end + end + + describe '#close' do + it 'updates metrics' do + user = create(:user) + service = described_class.new(metrics) + event = double(Event, author_id: user.id, created_at: Time.now) + + service.close(event) + + expect(metrics.latest_closed_by).to eq(user) + expect(metrics.latest_closed_at).to eq(event.created_at) + end + end + + describe '#reopen' do + it 'updates metrics' do + service = described_class.new(metrics) + + service.reopen + + expect(metrics.latest_closed_by).to be_nil + expect(metrics.latest_closed_at).to be_nil + end + end +end diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 58a5bede3de..bb3d73edf8e 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -15,7 +15,7 @@ describe Users::DestroyService do expect { user_data['email'].to eq(user.email) } expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) - expect { Namespace.with_deleted.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'will delete the project' do @@ -188,5 +188,22 @@ describe Users::DestroyService do end end end + + describe "calls the before/after callbacks" do + it 'of project_members' do + expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once + + service.execute(user) + end + + it 'of group_members' do + group_member = create(:group_member) + group_member.group.group_members.create(user: user, access_level: 40) + + expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once + + service.execute(user) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f51bb44086b..85de0a14631 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -97,13 +97,19 @@ RSpec.configure do |config| TestEnv.init end - config.after(:suite) do - TestEnv.cleanup - end - config.before(:example) do # Skip pre-receive hook check so we can use the web editor and merge. allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + + allow_any_instance_of(Gitlab::Git::GitlabProjects).to receive(:fork_repository).and_wrap_original do |m, *args| + m.call(*args) + + shard_path, repository_relative_path = args + # We can't leave the hooks in place after a fork, as those would fail in tests + # The "internal" API is not available + FileUtils.rm_rf(File.join(shard_path, repository_relative_path, 'hooks')) + end + # Enable all features by default for testing allow(Feature).to receive(:enabled?) { true } end diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb new file mode 100644 index 00000000000..943c1f6ffd7 --- /dev/null +++ b/spec/support/api/boards_shared_examples.rb @@ -0,0 +1,180 @@ +shared_examples_for 'group and project boards' do |route_definition, ee = false| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + before do + board_parent.add_reporter(user) + board_parent.add_guest(guest) + end + + def expect_schema_match_for(response, schema_file, ee) + if ee + expect(response).to match_response_schema(schema_file, dir: "ee") + else + expect(response).to match_response_schema(schema_file) + end + end + + describe "GET #{route_definition}" do + context "when unauthenticated" do + it "returns authentication error" do + get api(root_url) + + expect(response).to have_gitlab_http_status(401) + end + end + + context "when authenticated" do + it "returns the issue boards" do + get api(root_url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect_schema_match_for(response, 'public_api/v4/boards', ee) + end + + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'get a single board by id' do + get api(url, user) + + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + end + end + + describe "GET #{route_definition}/:board_id/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns issue board lists' do + get api(url, 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.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get api("#{root_url}/22343/lists", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET #{route_definition}/:board_id/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns a list' do + get api("#{url}/#{dev_list.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(dev_list.id) + expect(json_response['label']['name']).to eq(dev_label.title) + expect(json_response['position']).to eq(1) + end + + it 'returns 404 if list not found' do + get api("#{url}/5324", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "POST #{route_definition}/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'creates a new issue board list for labels' do + post api(url, user), label_id: ux_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(ux_label.title) + expect(json_response['position']).to eq(3) + end + + it 'returns 400 when creating a new list if label_id is invalid' do + post api(url, user), label_id: 23423 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 403 for members with guest role' do + put api("#{url}/#{test_list.id}", guest), position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "updates a list" do + put api("#{url}/#{test_list.id}", user), + position: 1 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['position']).to eq(1) + end + + it "returns 404 error if list id not found" do + put api("#{url}/44444", user), + position: 1 + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 403 for members with guest role" do + put api("#{url}/#{test_list.id}", guest), + position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "DELETE #{route_definition}/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete api("#{url}/#{dev_list.id}", non_member) + + expect(response).to have_gitlab_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete api("#{url}/#{dev_list.id}", guest) + + expect(response).to have_gitlab_http_status(403) + end + + it "returns 404 error if list id not found" do + delete api("#{url}/44444", user) + + expect(response).to have_gitlab_http_status(404) + end + + context "when the user is parent owner" do + set(:owner) { create(:user) } + + before do + if board_parent.try(:namespace) + board_parent.update(namespace: owner.namespace) + else + board.parent.add_owner(owner) + end + end + + it "deletes the list if an admin requests it" do + delete api("#{url}/#{dev_list.id}", owner) + + expect(response).to have_gitlab_http_status(204) + end + + it_behaves_like '412 response' do + let(:request) { api("#{url}/#{dev_list.id}", owner) } + end + end + end +end diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index d9080b02541..0388f110d71 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -258,7 +258,7 @@ shared_examples_for 'group and project milestones' do |route_definition| # Add public project to the group in context setup_for_group if context_group - public_project.team << [user, :developer] + public_project.add_developer(user) milestone.issues << issue << confidential_issue end @@ -275,7 +275,7 @@ shared_examples_for 'group and project milestones' do |route_definition| it 'does not return confidential issues to team members with guest role' do member = create(:user) - public_project.team << [member, :guest] + public_project.add_guest(member) get api(issues_route, member) diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/background_migrations_matchers.rb index 423c0e4cefc..f4127efc6ae 100644 --- a/spec/support/background_migrations_matchers.rb +++ b/spec/support/background_migrations_matchers.rb @@ -1,4 +1,4 @@ -RSpec::Matchers.define :be_scheduled_migration do |delay, *expected| +RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected| match do |migration| BackgroundMigrationWorker.jobs.any? do |job| job['args'] == [migration, expected] && @@ -11,3 +11,16 @@ RSpec::Matchers.define :be_scheduled_migration do |delay, *expected| 'not scheduled in expected time!' end end + +RSpec::Matchers.define :be_scheduled_migration do |*expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args'] + args == [migration, expected] + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 935b170a0f6..5189c57b7db 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -33,6 +33,9 @@ Capybara.register_driver :chrome do |app| options.add_argument("disable-gpu") end + # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 + options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] + Capybara::Selenium::Driver.new( app, browser: :chrome, diff --git a/spec/support/cluster_application_spec.rb b/spec/support/cluster_application_spec.rb new file mode 100644 index 00000000000..ab77910a050 --- /dev/null +++ b/spec/support/cluster_application_spec.rb @@ -0,0 +1,105 @@ +shared_examples 'cluster application specs' do + let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") } + + describe '#name' do + it 'is .application_name' do + expect(subject.name).to eq(described_class.application_name) + end + + it 'is recorded in Clusters::Cluster::APPLICATIONS' do + expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) + end + end + + describe '#status' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { described_class.new(cluster: cluster) } + + it 'defaults to :not_installable' do + expect(subject.status_name).to be(:not_installable) + end + + context 'when application helm is scheduled' do + before do + create(factory_name, :scheduled, cluster: cluster) + end + + it 'defaults to :not_installable' do + expect(subject.status_name).to be(:not_installable) + end + end + + context 'when application helm is installed' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + it 'defaults to :installable' do + expect(subject.status_name).to be(:installable) + end + end + end + + describe '#install_command' do + it 'has all the needed information' do + expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false) + end + end + + describe 'status state machine' do + describe '#make_installing' do + subject { create(factory_name, :scheduled) } + + it 'is installing' do + subject.make_installing! + + expect(subject).to be_installing + end + end + + describe '#make_installed' do + subject { create(factory_name, :installing) } + + it 'is installed' do + subject.make_installed + + expect(subject).to be_installed + end + end + + describe '#make_errored' do + subject { create(factory_name, :installing) } + let(:reason) { 'some errors' } + + it 'is errored' do + subject.make_errored(reason) + + expect(subject).to be_errored + expect(subject.status_reason).to eq(reason) + end + end + + describe '#make_scheduled' do + subject { create(factory_name, :installable) } + + it 'is scheduled' do + subject.make_scheduled + + expect(subject).to be_scheduled + end + + describe 'when was errored' do + subject { create(factory_name, :errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end + end + end +end diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb index 224619c899c..d72925e1838 100644 --- a/spec/support/cookie_helper.rb +++ b/spec/support/cookie_helper.rb @@ -8,6 +8,10 @@ module CookieHelper page.driver.browser.manage.add_cookie(name: name, value: value, **options) end + def get_cookie(name) + page.driver.browser.manage.cookie_named(name) + end + private def on_a_page? diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 26fd271ce31..d5ef80cfab2 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -5,10 +5,16 @@ module CycleAnalyticsHelpers end def create_commit(message, project, user, branch_name, count: 1) - oldrev = project.repository.commit(branch_name).sha + repository = project.repository + oldrev = repository.commit(branch_name).sha + + if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files) + mock_gitaly_multi_action_dates(repository.raw) + end + commit_shas = Array.new(count) do |index| - commit_sha = project.repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name) - project.repository.commit(commit_sha) + commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name) + repository.commit(commit_sha) commit_sha end @@ -98,6 +104,25 @@ module CycleAnalyticsHelpers pipeline: dummy_pipeline, protected: false) end + + def mock_gitaly_multi_action_dates(raw_repository) + allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args| + new_date = Time.now + branch_update = m.call(*args) + + if branch_update.newrev + _, opts = args + commit = raw_repository.commit(branch_update.newrev).rugged_commit + branch_update.newrev = commit.amend( + update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}", + author: commit.author.merge(time: new_date), + committer: commit.committer.merge(time: new_date) + ) + end + + branch_update + end + end end RSpec.configure do |config| diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index edaee03ea6c..1809ae1d141 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -1,10 +1,26 @@ +require 'database_cleaner/active_record/deletion' + +module FakeInformationSchema + # Work around a bug in DatabaseCleaner when using the deletion strategy: + # https://github.com/DatabaseCleaner/database_cleaner/issues/347 + # + # On MySQL, if the information schema is said to exist, we use an inaccurate + # row count leading to some tables not being cleaned when they should + def information_schema_exists?(_connection) + false + end +end + +DatabaseCleaner::ActiveRecord::Deletion.prepend(FakeInformationSchema) + RSpec.configure do |config| + # Ensure all sequences are reset at the start of the suite run config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end config.append_after(:context) do - DatabaseCleaner.clean_with(:truncation, cache_tables: false) + DatabaseCleaner.clean_with(:deletion, cache_tables: false) end config.before(:each) do @@ -12,15 +28,15 @@ RSpec.configure do |config| end config.before(:each, :js) do - DatabaseCleaner.strategy = :truncation + DatabaseCleaner.strategy = :deletion end - config.before(:each, :truncate) do - DatabaseCleaner.strategy = :truncation + config.before(:each, :delete) do + DatabaseCleaner.strategy = :deletion end config.before(:each, :migration) do - DatabaseCleaner.strategy = :truncation, { cache_tables: false } + DatabaseCleaner.strategy = :deletion, { cache_tables: false } end config.before(:each) do diff --git a/spec/support/devise_helpers.rb b/spec/support/devise_helpers.rb index 890a2d9d287..66874e10f38 100644 --- a/spec/support/devise_helpers.rb +++ b/spec/support/devise_helpers.rb @@ -2,13 +2,16 @@ module DeviseHelpers # explicitly tells Devise which mapping to use # this is needed when we are testing a Devise controller bypassing the router def set_devise_mapping(context:) - env = - if context.respond_to?(:env_config) - context.env_config - elsif context.respond_to?(:env) - context.env - end + env = env_from_context(context) env['devise.mapping'] = Devise.mappings[:user] if env end + + def env_from_context(context) + if context.respond_to?(:env_config) + context.env_config + elsif context.respond_to?(:env) + context.env + end + end end diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb index b39052923dd..1fb8252459f 100644 --- a/spec/support/email_helpers.rb +++ b/spec/support/email_helpers.rb @@ -30,4 +30,8 @@ module EmailHelpers def email_recipients(kind: :to) ActionMailer::Base.deliveries.flat_map(&kind) end + + def find_email_for(user) + ActionMailer::Base.deliveries.find { |d| d.to.include?(user.notification_email) } + end end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index c24940393f9..c8662d41769 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -113,6 +113,7 @@ shared_examples 'discussion comments' do |resource_name| else expect(find(submit_selector).value).to eq 'Start discussion' end + expect(page).not_to have_selector menu_selector end @@ -142,15 +143,17 @@ shared_examples 'discussion comments' do |resource_name| end if resource_name == 'merge request' + let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] } + it 'shows resolved discussion when toggled' do click_button "Resolve discussion" - expect(page).to have_selector('.note-row-1', visible: true) + expect(page).to have_selector(".note-row-#{note_id}", visible: true) refresh click_button "Toggle discussion" - expect(page).to have_selector('.note-row-1', visible: true) + expect(page).to have_selector(".note-row-#{note_id}", visible: true) end end end @@ -200,6 +203,7 @@ shared_examples 'discussion comments' do |resource_name| else expect(find(submit_selector).value).to eq 'Comment' end + expect(page).not_to have_selector menu_selector end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 08e21ee2537..2c20821ac3f 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -19,7 +19,7 @@ shared_examples 'issuable record that supports quick actions in its description let(:new_url_opts) { {} } before do - project.team << [master, :master] + project.add_master(master) gitlab_sign_in(master) end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 05021ea9054..f3f96bd1f0a 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -61,9 +61,11 @@ module FilteredSearchHelpers token_emoji = tokens[index][:emoji_name] expect(el.find('.name')).to have_content(token_name) + if token_value expect(el.find('.value')).to have_content(token_value) end + # gl-emoji content is blank when the emoji unicode is not supported if token_emoji selector = %(gl-emoji[data-name="#{token_emoji}"]) diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index 4ee33f9725b..876b3b8242d 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -24,6 +24,7 @@ def main unless system(*%W[git clone --bare #{SOURCE} #{REPO_NAME}], chdir: dir) abort "git clone failed" end + repo = File.join(dir, REPO_NAME) erb = ERB.new(DATA.read) erb.run(binding) diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index 8a073e58db8..2fdbddd40c2 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -10,6 +10,16 @@ module GoogleApi request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s end + def stub_cloud_platform_projects_list(options) + WebMock.stub_request(:get, cloud_platform_projects_list_url) + .to_return(cloud_platform_response(cloud_platform_projects_body(options))) + end + + def stub_cloud_platform_projects_get_billing_info(project_id, billing_enabled) + WebMock.stub_request(:get, cloud_platform_projects_get_billing_info_url(project_id)) + .to_return(cloud_platform_response(cloud_platform_projects_billing_info_body(project_id, billing_enabled))) + end + def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options) WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id)) .to_return(cloud_platform_response(cloud_platform_cluster_body(options))) @@ -40,6 +50,14 @@ module GoogleApi .to_return(status: [500, "Internal Server Error"]) end + def cloud_platform_projects_list_url + "https://cloudresourcemanager.googleapis.com/v1/projects" + end + + def cloud_platform_projects_get_billing_info_url(project_id) + "https://cloudbilling.googleapis.com/v1/projects/#{project_id}/billingInfo" + end + def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id) "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}" end @@ -115,5 +133,32 @@ module GoogleApi "endTime": options[:endTime] || '' } end + + def cloud_platform_projects_body(**options) + { + "projects": [ + { + "projectNumber": options[:project_number] || "1234", + "projectId": options[:project_id] || "test-project-1234", + "lifecycleState": "ACTIVE", + "name": options[:name] || "test-project", + "createTime": "2017-12-16T01:48:29.129Z", + "parent": { + "type": "organization", + "id": "12345" + } + } + ] + } + end + + def cloud_platform_projects_billing_info_body(project_id, billing_enabled) + { + "name": "projects/#{project_id}/billingInfo", + "projectId": "#{project_id}", + "billingAccountName": "account-name", + "billingEnabled": billing_enabled + } + end end end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 923c8080e6c..2197bc9d853 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -1,6 +1,5 @@ require 'action_dispatch/testing/test_request' require 'fileutils' -require 'gitlab/popen' module JavaScriptFixturesHelpers include Gitlab::Popen diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 50702a0ac88..b52b6a28c54 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -125,6 +125,13 @@ module LoginHelpers }) end + def stub_omniauth_provider(provider, context: Rails.application) + env = env_from_context(context) + + set_devise_mapping(context: context) + env['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + end + def stub_omniauth_saml_config(messages) set_devise_mapping(context: Rails.application) Rails.application.routes.disable_clear_and_finalize = true diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index a0d854d3641..39e94ad53de 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -24,7 +24,7 @@ class MarkdownFeature def project @project ||= create(:project, :repository, group: group).tap do |project| - project.team << [user, :master] + project.add_master(user) end end @@ -85,7 +85,7 @@ class MarkdownFeature @xproject ||= begin group = create(:group, :nested) create(:project, :repository, namespace: group) do |project| - project.team << [user, :developer] + project.add_developer(user) end end end diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index cdb62a5deee..42a9ed9ff34 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -43,6 +43,7 @@ module AccessMatchersForController user = create(:user) membership.public_send(:"add_#{role}", user) end + user end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index d12b2757427..ec4ec6f4038 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -190,6 +190,27 @@ module MarkdownMatchers expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4') end end + + # ColorFilter + matcher :parse_colors do + set_default_markdown_messages + + match do |actual| + color_chips = actual.css('code > span.gfm-color_chip > span') + + expect(color_chips.count).to eq(9) + + [ + '#F00', '#F00A', '#FF0000', '#FF0000AA', 'RGB(0,255,0)', + 'RGB(0%,100%,0%)', 'RGBA(0,255,0,0.7)', 'HSL(540,70%,50%)', + 'HSLA(540,70%,50%,0.7)' + ].each_with_index do |color, i| + parsed_color = Banzai::ColorParser.parse(color) + expect(color_chips[i]['style']).to match("background-color: #{parsed_color};") + expect(color_chips[i].parent.parent.content).to match(color) + end + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 3ac201f1fb1..1685decbe94 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -53,7 +53,7 @@ shared_context 'mentionable context' do set_mentionable_text.call(ref_string) - project.team << [author, :developer] + project.add_developer(author) end end diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb index d6680735aa1..2c501a2a27c 100644 --- a/spec/support/project_forks_helper.rb +++ b/spec/support/project_forks_helper.rb @@ -38,10 +38,6 @@ module ProjectForksHelper # so we have to explicitely call this method to clear the @exists variable. # of the instance we're returning here. forked_project.repository.after_import - - # We can't leave the hooks in place after a fork, as those would fail in tests - # The "internal" API is not available - FileUtils.rm_rf("#{forked_project.repository.path}/hooks") end forked_project diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb index bd83cb88058..baf8bcc04b8 100644 --- a/spec/support/reference_parser_shared_examples.rb +++ b/spec/support/reference_parser_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features| end it "creates reference for member" do - project.team << [user, :developer] + project.add_developer(user) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index 55da961e173..90618ba5b19 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,6 +17,7 @@ module Select2Helper selector = options.fetch(:from) first(selector, visible: false) + if options[:multiple] execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');") else diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index 9399745f900..7b064162726 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -3,7 +3,7 @@ shared_examples 'new issuable record that supports quick actions' do let!(:project) { create(:project, :repository) } - let(:user) { create(:user).tap { |u| project.team << [u, :master] } } + let(:user) { create(:user).tap { |u| project.add_master(u) } } let(:assignee) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:labels) { create_list(:label, 3, project: project) } @@ -12,7 +12,7 @@ shared_examples 'new issuable record that supports quick actions' do let(:issuable) { described_class.new(project, user, params).execute } before do - project.team << [assignee, :master] + project.add_master(assignee) end context 'with labels in command only' do diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 7457484a932..23f9b46ae0c 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -3,13 +3,9 @@ Service.available_services_names.each do |service| let(:dashed_service) { service.dasherize } let(:service_method) { "#{service}_service".to_sym } let(:service_klass) { "#{service}_service".classify.constantize } - let(:service_fields) { service_klass.new.fields } + let(:service_instance) { service_klass.new } + let(:service_fields) { service_instance.fields } let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } - let(:service_attrs_list_without_passwords) do - service_fields - .select { |field| field[:type] != 'password' } - .map { |field| field[:name].to_sym} - end let(:service_attrs) do service_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/ @@ -29,5 +25,13 @@ Service.available_services_names.each do |service| end end end + + def initialize_service(service) + service_item = project.find_or_initialize_service(service) + service_item.properties = service_attrs + service_item.active = true if service == "kubernetes" + service_item.save + service_item + end end end diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 935c08221e0..7ce80c82439 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -2,6 +2,8 @@ shared_examples 'handle uploads' do let(:user) { create(:user) } let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + let(:secret) { FileUploader.generate_secret } + let(:uploader_class) { FileUploader } describe "POST #create" do context 'when a user is not authorized to upload a file' do @@ -65,7 +67,12 @@ shared_examples 'handle uploads' do describe "GET #show" do let(:show_upload) do - get :show, params.merge(secret: "123456", filename: "image.jpg") + get :show, params.merge(secret: secret, filename: "rails_sample.jpg") + end + + before do + expect(FileUploader).to receive(:generate_secret).and_return(secret) + UploadService.new(model, jpg, uploader_class).execute end context "when the model is public" do @@ -75,11 +82,6 @@ shared_examples 'handle uploads' do context "when not signed in" do context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - it "responds with status 200" do show_upload @@ -88,6 +90,10 @@ shared_examples 'handle uploads' do end context "when the file doesn't exist" do + before do + allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) + end + it "responds with status 404" do show_upload @@ -102,11 +108,6 @@ shared_examples 'handle uploads' do end context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - it "responds with status 200" do show_upload @@ -115,6 +116,10 @@ shared_examples 'handle uploads' do end context "when the file doesn't exist" do + before do + allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) + end + it "responds with status 404" do show_upload @@ -131,11 +136,6 @@ shared_examples 'handle uploads' do context "when not signed in" do context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - context "when the file is an image" do before do allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) @@ -149,6 +149,10 @@ shared_examples 'handle uploads' do end context "when the file is not an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(false) + end + it "redirects to the sign in page" do show_upload @@ -158,6 +162,10 @@ shared_examples 'handle uploads' do end context "when the file doesn't exist" do + before do + allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) + end + it "redirects to the sign in page" do show_upload @@ -177,11 +185,6 @@ shared_examples 'handle uploads' do end context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - it "responds with status 200" do show_upload @@ -190,6 +193,10 @@ shared_examples 'handle uploads' do end context "when the file doesn't exist" do + before do + allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) + end + it "responds with status 404" do show_upload @@ -200,11 +207,6 @@ shared_examples 'handle uploads' do context "when the user doesn't have access to the model" do context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - context "when the file is an image" do before do allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) @@ -218,6 +220,10 @@ shared_examples 'handle uploads' do end context "when the file is not an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(false) + end + it "responds with status 404" do show_upload @@ -227,6 +233,10 @@ shared_examples 'handle uploads' do end context "when the file doesn't exist" do + before do + allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) + end + it "responds with status 404" do show_upload 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 new file mode 100644 index 00000000000..5b0b609f7f2 --- /dev/null +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -0,0 +1,99 @@ +RSpec.shared_examples 'a creatable merge request' do + include WaitForRequests + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + + before do + source_project.add_master(user) + target_project.add_master(user) + target_project.add_master(user2) + + sign_in(user) + visit project_new_merge_request_path( + target_project, + merge_request: { + source_project_id: source_project.id, + target_project_id: target_project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end + + it 'creates new merge request', :js do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user2.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user2.name + end + + click_link 'Assign to me' + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit merge request' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + + it 'updates the branches when selecting a new target project' do + target_project_member = target_project.owner + CreateBranchService.new(target_project, target_project_member) + .execute('a-brand-new-branch-to-test', 'master') + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + wait_for_requests + + first('.js-target-branch').click + + within('.dropdown-target-branch .dropdown-content') do + expect(page).to have_content('a-brand-new-branch-to-test') + end + end +end diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb new file mode 100644 index 00000000000..645db41cddc --- /dev/null +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -0,0 +1,140 @@ +RSpec.shared_examples 'an editable merge request' do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let(:merge_request) do + create(:merge_request, + source_project: source_project, + target_project: target_project, + source_branch: 'fix', + target_branch: 'master') + end + + before do + source_project.add_master(user) + target_project.add_master(user) + target_project.add_master(user2) + + sign_in(user) + visit edit_project_merge_request_path(target_project, merge_request) + end + + it 'updates merge request', :js do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + page.within '.js-label-select' do + expect(page).to have_content label.title + end + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + + it 'description has autocomplete', :js do + find('#merge_request_description').native.send_keys('') + fill_in 'merge_request_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end + + it 'has class js-quick-submit in form' do + expect(page).to have_selector('.js-quick-submit') + end + + it 'warns about version conflict' do + merge_request.update(title: "New title") + + fill_in 'merge_request_title', with: 'bug 345' + fill_in 'merge_request_description', with: 'bug description' + + click_button 'Save changes' + + expect(page).to have_content 'Someone edited the merge request the same time you did' + end + + it 'preserves description textarea height', :js do + long_description = %q( + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat. + + Cras congue nec ligula tristique viverra. Curabitur fringilla fringilla fringilla. Donec rhoncus dignissim orci ut accumsan. Ut rutrum urna a rhoncus varius. Maecenas blandit, mauris nec accumsan gravida, augue nibh finibus magna, sed maximus turpis libero nec neque. Suspendisse at semper est. Nunc imperdiet dapibus dui, varius sollicitudin erat luctus non. Sed pellentesque ligula eget posuere facilisis. Donec dictum commodo volutpat. Donec egestas dui ac magna sollicitudin bibendum. Vivamus purus neque, ullamcorper ac feugiat et, tempus sit amet metus. Praesent quis viverra neque. Sed bibendum viverra est, eu aliquam mi ornare vitae. Proin et dapibus ipsum. Nunc tortor diam, malesuada nec interdum vel, placerat quis justo. Ut viverra at erat eu laoreet. + + Pellentesque commodo, diam sit amet dignissim condimentum, tortor justo pretium est, non venenatis metus eros ut nunc. Etiam ut neque eget sem dapibus aliquam. Curabitur vel elit lorem. Nulla nec enim elit. Sed ut ex id justo facilisis convallis at ac augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nullam cursus egestas turpis non tristique. Suspendisse in erat sem. Fusce libero elit, fermentum gravida mauris id, auctor iaculis felis. Nullam vulputate tempor laoreet. + + Nam tempor et magna sed convallis. Fusce sit amet sollicitudin risus, a ullamcorper lacus. Morbi gravida quis sem eget porttitor. Donec eu egestas mauris, in elementum tortor. Sed eget ex mi. Mauris iaculis tortor ut est auctor, nec dignissim quam sagittis. Suspendisse vel metus non quam suscipit tincidunt. Cras molestie lacus non justo finibus sodales quis vitae erat. In a porttitor nisi, id sollicitudin urna. Ut at felis tellus. Suspendisse potenti. + + Maecenas leo ligula, varius at neque vitae, ornare maximus justo. Nullam convallis luctus risus et vulputate. Duis suscipit faucibus iaculis. Etiam quis tortor faucibus, tristique tellus sit amet, sodales neque. Nulla dapibus nisi vel aliquet consequat. Etiam faucibus, metus eget condimentum iaculis, enim urna lobortis sem, id efficitur eros sapien nec nisi. Aenean ut finibus ex. + ) + + fill_in 'merge_request_description', with: long_description + + height = get_textarea_height + find('.js-md-preview-button').click + find('.js-md-write-button').click + new_height = get_textarea_height + + expect(height).to eq(new_height) + end + + context 'when "Remove source branch" is set' do + before do + merge_request.update!(merge_params: { 'force_remove_source_branch' => '1' }) + end + + it 'allows to unselect "Remove source branch"', :js do + expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy + + visit edit_project_merge_request_path(target_project, merge_request) + uncheck 'Remove source branch when merge request is accepted' + + click_button 'Save changes' + + expect(page).to have_unchecked_field 'remove-source-branch-input' + expect(page).to have_content 'Remove source branch' + end + end +end + +def get_textarea_height + page.evaluate_script('document.getElementById("merge_request_description").offsetHeight') +end diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb new file mode 100644 index 00000000000..96d59e0c472 --- /dev/null +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -0,0 +1,29 @@ +shared_examples 'issuable participants endpoint' do + let(:area) { entity.class.name.underscore.pluralize } + + it 'returns participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", 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.size).to eq(entity.participants.size) + + last_participant = entity.participants.last + expect(json_response.last['id']).to eq(last_participant.id) + expect(json_response.last['name']).to eq(last_participant.name) + expect(json_response.last['username']).to eq(last_participant.username) + end + + it 'returns a 404 when iid does not exist' do + get api("/projects/#{project.id}/#{area}/999/participants", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns a 404 when id is used instead of iid' do + get api("/projects/#{project.id}/#{area}/#{entity.id}/participants", user) + + expect(response).to have_gitlab_http_status(404) + end +end diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb new file mode 100644 index 00000000000..934d53e7bba --- /dev/null +++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb @@ -0,0 +1,48 @@ +shared_examples "matches the method pattern" do |method| + let(:target) { subject } + let(:args) { nil } + let(:pattern) { patterns[method] } + + it do + return skip "No pattern provided, skipping." unless pattern + + expect(target.method(method).call(*args)).to match(pattern) + end +end + +shared_examples "builds correct paths" do |**patterns| + let(:patterns) { patterns } + + before do + allow(subject).to receive(:filename).and_return('<filename>') + end + + describe "#store_dir" do + it_behaves_like "matches the method pattern", :store_dir + end + + describe "#cache_dir" do + it_behaves_like "matches the method pattern", :cache_dir + end + + describe "#work_dir" do + it_behaves_like "matches the method pattern", :work_dir + end + + describe "#upload_path" do + it_behaves_like "matches the method pattern", :upload_path + end + + describe ".absolute_path" do + it_behaves_like "matches the method pattern", :absolute_path do + let(:target) { subject.class } + let(:args) { [upload] } + end + end + + describe ".base_dir" do + it_behaves_like "matches the method pattern", :base_dir do + let(:target) { subject.class } + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 17f3a861ba8..e827a8da0b7 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -57,6 +57,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do @issue = issue_service.execute @issues_sample_data = issue_service.hook_data(@issue, 'open') + project.add_developer(user) opts = { title: 'Awesome merge_request', description: 'please fix', diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index f9121cce985..52e47ae2d34 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -15,9 +15,7 @@ RSpec.configure do |config| # Track the maximum number of failures first_failure = Time.parse("2017-11-14 17:52:30") last_failure = Time.parse("2017-11-14 18:54:37") - failure_count = Gitlab::CurrentSettings - .current_application_settings - .circuitbreaker_failure_count_threshold + 1 + failure_count = Gitlab::CurrentSettings.circuitbreaker_failure_count_threshold + 1 cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}" Gitlab::Git::Storage.redis.with do |redis| diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index f621463e621..36b90fc68d6 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -1,9 +1,8 @@ # Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb module StubENV - include Gitlab::CurrentSettings - def stub_env(key_or_hash, value = nil) init_stub unless env_stubbed? + if key_or_hash.is_a? Hash key_or_hash.each { |k, v| add_stubbed_value(k, v) } else diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index ffc051a3fff..c275522159c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -1,4 +1,5 @@ require 'rspec/mocks' +require 'toml' module TestEnv extend self @@ -19,7 +20,7 @@ module TestEnv 'improve/awesome' => '5937ac0', 'merged-target' => '21751bf', 'markdown' => '0ed8c6c', - 'lfs' => 'be93687', + 'lfs' => '55bc176', 'master' => 'b83d6e3', 'merge-test' => '5937ac0', "'test'" => 'e56497b', @@ -89,10 +90,6 @@ module TestEnv setup_forked_repo end - def cleanup - stop_gitaly - end - def disable_mailer allow_any_instance_of(NotificationService).to receive(:mailer) .and_return(double.as_null_object) @@ -147,6 +144,9 @@ module TestEnv version: Gitlab::GitalyClient.expected_server_version, task: "gitlab:gitaly:install[#{gitaly_dir}]") do + # Always re-create config, in case it's outdated. This is fast anyway. + Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true) + start_gitaly(gitaly_dir) end end @@ -159,6 +159,8 @@ module TestEnv spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + Kernel.at_exit { stop_gitaly } + wait_gitaly end @@ -215,7 +217,7 @@ module TestEnv end def copy_repo(project, bare_repo:, refs:) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path @@ -235,7 +237,7 @@ module TestEnv end def artifacts_path - Gitlab.config.artifacts.path + Gitlab.config.artifacts.storage_path end # When no cached assets exist, manually hit the root path to create them @@ -305,7 +307,7 @@ module TestEnv # Before we used Git clone's --mirror option, bare repos could end up # with missing refs, clearing them and retrying should fix the issue. - cleanup && clean_gitlab_test_path && init unless reset.call + clean_gitlab_test_path && init unless reset.call end end @@ -321,6 +323,7 @@ module TestEnv if component_needs_update?(install_dir, version) # Cleanup the component entirely to ensure we start fresh FileUtils.rm_rf(install_dir) + unless system('rake', task) raise ComponentFailedToInstallError end @@ -347,6 +350,9 @@ module TestEnv end def component_needs_update?(component_folder, expected_version) + # Allow local overrides of the component for tests during development + return false if Rails.env.test? && File.symlink?(component_folder) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb index d05eda08201..5752078d2a0 100644 --- a/spec/support/track_untracked_uploads_helpers.rb +++ b/spec/support/track_untracked_uploads_helpers.rb @@ -1,6 +1,6 @@ module TrackUntrackedUploadsHelpers def uploaded_file - fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg') fixture_file_upload(fixture_path) end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 3d9705c9c05..e5c8ac6a004 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -9,7 +9,7 @@ shared_context 'unique ips sign in limit' do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - current_application_settings.update!( + Gitlab::CurrentSettings.update!( unique_ips_limit_enabled: true, unique_ips_limit_time_window: 10000 ) @@ -34,7 +34,7 @@ end shared_examples 'user login operation with unique ip limit' do include_context 'unique ips sign in limit' do before do - current_application_settings.update!(unique_ips_limit_per_user: 1) + Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1) end it 'allows user authenticating from the same ip' do @@ -52,7 +52,7 @@ end shared_examples 'user login request with unique ip limit' do |success_status = 200| include_context 'unique ips sign in limit' do before do - current_application_settings.update!(unique_ips_limit_per_user: 1) + Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1) end it 'allows user authenticating from the same ip' do diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb index 565d3203e4f..5e3f19ba19e 100644 --- a/spec/support/updating_mentions_shared_examples.rb +++ b/spec/support/updating_mentions_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'updating mentions' do |service_class| let(:service_class) { service_class } before do - project.team << [mentioned_user, :developer] + project.add_developer(mentioned_user) end def update_mentionable(opts) diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index f4130d68271..fda0e29f983 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -53,6 +53,7 @@ module WaitForRequests wait_until = Time.now + max_wait_time.seconds loop do break if yield + if Time.now > wait_until raise "Condition not met: #{condition_name}" else diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index b41c3b3958a..168facd51a6 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -165,7 +165,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('pages.tar.gz') expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).to match('registry.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) + expect(tar_contents).not_to match(%r{^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)/$}) end it 'deletes temp directories' do diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb new file mode 100644 index 00000000000..9aebf7b0b4a --- /dev/null +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -0,0 +1,38 @@ +require 'rake_helper' + +describe 'gitlab:git rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/git' + + storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } + + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git')) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow_any_instance_of(String).to receive(:color) { |string, _color| string } + + stub_warn_user_is_not_gitlab + end + + after do + FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + end + + describe 'fsck' do + it 'outputs the integrity check for a repo' do + expect { run_rake_task('gitlab:git:fsck') }.to output(%r{Performed Checking integrity at .*@hashed/1/2/test.git}).to_stdout + end + + it 'errors out about config.lock issues' do + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/config.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/file exists\? ... yes/).to_stdout + end + + it 'errors out about ref lock issues' do + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads')) + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads/blah.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout + end + end +end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 6aba86fdc3c..b37d6ac831f 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do end context 'when Rails.env is test' do - let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] } + let(:command) do + %W[make + BUNDLE_FLAGS=--no-deployment + BUNDLE_PATH=#{Bundler.bundle_path}] + end before do allow(Rails.env).to receive(:test?).and_return(true) diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index fae5ec35c47..e9322ec4931 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'tasks/gitlab/task_helpers' class TestHelpersTest include Gitlab::TaskHelpers diff --git a/spec/tasks/gitlab/uploads_rake_spec.rb b/spec/tasks/gitlab/uploads_rake_spec.rb new file mode 100644 index 00000000000..ac0005e51e0 --- /dev/null +++ b/spec/tasks/gitlab/uploads_rake_spec.rb @@ -0,0 +1,27 @@ +require 'rake_helper' + +describe 'gitlab:uploads rake tasks' do + describe 'check' do + let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) } + + before do + Rake.application.rake_require 'tasks/gitlab/uploads' + end + + it 'outputs the integrity check for each uploaded file' do + expect { run_rake_task('gitlab:uploads:check') }.to output(/Checking file \(#{upload.id}\): #{Regexp.quote(upload.absolute_path)}/).to_stdout + end + + it 'errors out about missing files on the file system' do + create(:upload) + + expect { run_rake_task('gitlab:uploads:check') }.to output(/File does not exist on the file system/).to_stdout + end + + it 'errors out about invalid checksum' do + upload.update_column(:checksum, '01a3156db2cf4f67ec823680b40b7302f89ab39179124ad219f94919b8a1769e') + + expect { run_rake_task('gitlab:uploads:check') }.to output(/File checksum \(9e697aa09fe196909813ee36103e34f721fe47a5fdc8aac0e4e4ac47b9b38282\) does not match the one in the database \(#{upload.checksum}\)/).to_stdout + end + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index 04ee6e9bfad..091ba824fc6 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -1,28 +1,14 @@ require 'spec_helper' describe AttachmentUploader do - let(:uploader) { described_class.new(build_stubbed(:user)) } + let(:note) { create(:note, :with_attachment) } + let(:uploader) { note.attachment } + let(:upload) { create(:upload, :attachment_upload, model: uploader.model) } - describe "#store_dir" do - it "stores in the system dir" do - expect(uploader.store_dir).to start_with("uploads/-/system/user") - end + subject { uploader } - it "uses the old path when using object storage" do - expect(described_class).to receive(:file_storage?).and_return(false) - expect(uploader.store_dir).to start_with("uploads/user") - end - end - - describe '#move_to_cache' do - it 'is true' do - expect(uploader.move_to_cache).to eq(true) - end - end - - describe '#move_to_store' do - it 'is true' do - expect(uploader.move_to_store).to eq(true) - end - end + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/note/attachment/], + upload_path: %r[uploads/-/system/note/attachment/], + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/] end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index 1dc574699d8..bf9028c9260 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -1,18 +1,16 @@ require 'spec_helper' describe AvatarUploader do - let(:uploader) { described_class.new(build_stubbed(:user)) } + let(:model) { create(:user, :with_avatar) } + let(:uploader) { described_class.new(model, :avatar) } + let(:upload) { create(:upload, model: model) } - describe "#store_dir" do - it "stores in the system dir" do - expect(uploader.store_dir).to start_with("uploads/-/system/user") - end + subject { uploader } - it "uses the old path when using object storage" do - expect(described_class).to receive(:file_storage?).and_return(false) - expect(uploader.store_dir).to start_with("uploads/user") - end - end + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/user/avatar/], + upload_path: %r[uploads/-/system/user/avatar/], + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/] describe '#move_to_cache' do it 'is false' do diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index 0cf462e9553..bc024cd307c 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe FileMover do let(:filename) { 'banana_sample.gif' } let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } + let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } + let(:temp_description) do - 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\ - '(/uploads/-/system/temp/secret55/banana_sample.gif)' + "test ![banana_sample](/#{temp_file_path}) "\ + "same ![banana_sample](/#{temp_file_path}) " end - let(:temp_file_path) { File.join('secret55', filename).to_s } - let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } - + let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) } let(:snippet) { create(:personal_snippet, description: temp_description) } subject { described_class.new(file_path, snippet).execute } @@ -28,8 +28,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\ + "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) " ) end @@ -50,8 +50,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "\ + "same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) " ) end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index fd195d6f9b8..a559681a079 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,118 +1,84 @@ require 'spec_helper' describe FileUploader do - let(:uploader) { described_class.new(build_stubbed(:project)) } + let(:group) { create(:group, name: 'awesome') } + let(:project) { create(:project, namespace: group, name: 'project') } + let(:uploader) { described_class.new(project) } + let(:upload) { double(model: project, path: 'secret/foo.jpg') } - context 'legacy storage' do - let(:project) { build_stubbed(:project) } - - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - upload = double(model: project, path: 'secret/foo.jpg') - - dynamic_segment = project.full_path - - expect(described_class.absolute_path(upload)) - .to end_with("#{dynamic_segment}/secret/foo.jpg") - end - end + subject { uploader } - describe "#store_dir" do - it "stores in the namespace path" do - uploader = described_class.new(project) - - expect(uploader.store_dir).to include(project.full_path) - expect(uploader.store_dir).not_to include("system") - end - end + shared_examples 'builds correct legacy storage paths' do + include_examples 'builds correct paths', + store_dir: %r{awesome/project/\h+}, + absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg} end - context 'hashed storage' do + shared_examples 'uses hashed storage' do context 'when rolled out attachments' do - let(:project) { build_stubbed(:project, :hashed) } - - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - upload = double(model: project, path: 'secret/foo.jpg') - - dynamic_segment = project.disk_path - - expect(described_class.absolute_path(upload)) - .to end_with("#{dynamic_segment}/secret/foo.jpg") - end + before do + allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed') end - describe "#store_dir" do - it "stores in the namespace path" do - uploader = described_class.new(project) + let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') } - expect(uploader.store_dir).to include(project.disk_path) - expect(uploader.store_dir).not_to include("system") - end - end + it_behaves_like 'builds correct paths', + store_dir: %r{ca/fe/fe/ed/\h+}, + absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg} end context 'when only repositories are rolled out' do - let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) } - - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - upload = double(model: project, path: 'secret/foo.jpg') + let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) } - dynamic_segment = project.full_path + it_behaves_like 'builds correct legacy storage paths' + end + end - expect(described_class.absolute_path(upload)) - .to end_with("#{dynamic_segment}/secret/foo.jpg") - end - end + context 'legacy storage' do + it_behaves_like 'builds correct legacy storage paths' + include_examples 'uses hashed storage' + end - describe "#store_dir" do - it "stores in the namespace path" do - uploader = described_class.new(project) + describe 'initialize' do + let(:uploader) { described_class.new(double, secret: 'secret') } - expect(uploader.store_dir).to include(project.full_path) - expect(uploader.store_dir).not_to include("system") - end - end + it 'accepts a secret parameter' do + expect(described_class).not_to receive(:generate_secret) + expect(uploader.secret).to eq('secret') end end - describe 'initialize' do + describe '#secret' do it 'generates a secret if none is provided' do - expect(SecureRandom).to receive(:hex).and_return('secret') - - uploader = described_class.new(double) - - expect(uploader.secret).to eq 'secret' + expect(described_class).to receive(:generate_secret).and_return('secret') + expect(uploader.secret).to eq('secret') end + end - it 'accepts a secret parameter' do - expect(SecureRandom).not_to receive(:hex) + describe '#upload=' do + let(:secret) { SecureRandom.hex } + let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') } - uploader = described_class.new(double, 'secret') + it 'handles nil' do + expect(uploader).not_to receive(:apply_context!) - expect(uploader.secret).to eq 'secret' + uploader.upload = nil end - end - describe '#move_to_cache' do - it 'is true' do - expect(uploader.move_to_cache).to eq(true) - end - end + it 'extract the uploader context from it' do + expect(uploader).to receive(:apply_context!).with(a_hash_including(secret: secret, identifier: 'file.txt')) - describe '#move_to_store' do - it 'is true' do - expect(uploader.move_to_store).to eq(true) + uploader.upload = upload end - end - describe '#relative_path' do - it 'removes the leading dynamic path segment' do - fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') - uploader.store!(fixture_file_upload(fixture)) + context 'uploader_context is empty' do + it 'fallbacks to regex based extraction' do + expect(upload).to receive(:uploader_context).and_return({}) - expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/) + uploader.upload = upload + expect(uploader.secret).to eq(secret) + expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt') + end end end end diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index a144b39f74f..60e35dcf235 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -4,7 +4,7 @@ require 'carrierwave/storage/fog' describe GitlabUploader do let(:uploader_class) { Class.new(described_class) } - subject { uploader_class.new } + subject { uploader_class.new(double) } describe '#file_storage?' do context 'when file storage is used' do diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index 14fd5f3600f..d606404e95d 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -3,33 +3,13 @@ require 'spec_helper' describe JobArtifactUploader do let(:job_artifact) { create(:ci_job_artifact) } let(:uploader) { described_class.new(job_artifact, :file) } - let(:local_path) { Gitlab.config.artifacts.path } - describe '#store_dir' do - subject { uploader.store_dir } + subject { uploader } - let(:path) { "#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/#{job_artifact.project_id}/#{job_artifact.id}" } - - context 'when using local storage' do - it { is_expected.to start_with(local_path) } - it { is_expected.to match(/\h{2}\/\h{2}\/\h{64}\/\d{4}_\d{1,2}_\d{1,2}\/\d+\/\d+\z/) } - it { is_expected.to end_with(path) } - end - end - - describe '#cache_dir' do - subject { uploader.cache_dir } - - it { is_expected.to start_with(local_path) } - it { is_expected.to end_with('/tmp/cache') } - end - - describe '#work_dir' do - subject { uploader.work_dir } - - it { is_expected.to start_with(local_path) } - it { is_expected.to end_with('/tmp/work') } - end + it_behaves_like "builds correct paths", + store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z], + cache_dir: %r[artifacts/tmp/cache], + work_dir: %r[artifacts/tmp/work] context 'file is stored in valid local_path' do let(:file) do @@ -43,9 +23,9 @@ describe JobArtifactUploader do subject { uploader.file.path } - it { is_expected.to start_with(local_path) } + it { is_expected.to start_with("#{uploader.root}/#{uploader.class.base_dir}") } it { is_expected.to include("/#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/") } - it { is_expected.to include("/#{job_artifact.project_id}/") } + it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") } it { is_expected.to end_with("ci_build_artifacts.zip") } end end diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb index efeffb78772..54c6a8b869b 100644 --- a/spec/uploaders/legacy_artifact_uploader_spec.rb +++ b/spec/uploaders/legacy_artifact_uploader_spec.rb @@ -3,49 +3,22 @@ require 'rails_helper' describe LegacyArtifactUploader do let(:job) { create(:ci_build) } let(:uploader) { described_class.new(job, :legacy_artifacts_file) } - let(:local_path) { Gitlab.config.artifacts.path } + let(:local_path) { described_class.root } - describe '.local_store_path' do - subject { described_class.local_store_path } + subject { uploader } - it "delegate to artifacts path" do - expect(Gitlab.config.artifacts).to receive(:path) - - subject - end - end - - describe '.artifacts_upload_path' do - subject { described_class.artifacts_upload_path } + # TODO: move to Workhorse::UploadPath + describe '.workhorse_upload_path' do + subject { described_class.workhorse_upload_path } it { is_expected.to start_with(local_path) } - it { is_expected.to end_with('tmp/uploads/') } - end - - describe '#store_dir' do - subject { uploader.store_dir } - - let(:path) { "#{job.created_at.utc.strftime('%Y_%m')}/#{job.project_id}/#{job.id}" } - - context 'when using local storage' do - it { is_expected.to start_with(local_path) } - it { is_expected.to end_with(path) } - end + it { is_expected.to end_with('tmp/uploads') } end - describe '#cache_dir' do - subject { uploader.cache_dir } - - it { is_expected.to start_with(local_path) } - it { is_expected.to end_with('/tmp/cache') } - end - - describe '#work_dir' do - subject { uploader.work_dir } - - it { is_expected.to start_with(local_path) } - it { is_expected.to end_with('/tmp/work') } - end + it_behaves_like "builds correct paths", + store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z], + cache_dir: %r[artifacts/tmp/cache], + work_dir: %r[artifacts/tmp/work] describe '#filename' do # we need to use uploader, as this makes to use mounter @@ -69,7 +42,7 @@ describe LegacyArtifactUploader do subject { uploader.file.path } - it { is_expected.to start_with(local_path) } + it { is_expected.to start_with("#{uploader.root}") } it { is_expected.to include("/#{job.created_at.utc.strftime('%Y_%m')}/") } it { is_expected.to include("/#{job.project_id}/") } it { is_expected.to end_with("ci_build_artifacts.zip") } diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb index 7088bc23334..6ebc885daa8 100644 --- a/spec/uploaders/lfs_object_uploader_spec.rb +++ b/spec/uploaders/lfs_object_uploader_spec.rb @@ -2,39 +2,13 @@ require 'spec_helper' describe LfsObjectUploader do let(:lfs_object) { create(:lfs_object, :with_file) } - let(:uploader) { described_class.new(lfs_object) } + let(:uploader) { described_class.new(lfs_object, :file) } let(:path) { Gitlab.config.lfs.storage_path } - describe '#move_to_cache' do - it 'is true' do - expect(uploader.move_to_cache).to eq(true) - end - end + subject { uploader } - describe '#move_to_store' do - it 'is true' do - expect(uploader.move_to_store).to eq(true) - end - end - - describe '#store_dir' do - subject { uploader.store_dir } - - it { is_expected.to start_with(path) } - it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") } - end - - describe '#cache_dir' do - subject { uploader.cache_dir } - - it { is_expected.to start_with(path) } - it { is_expected.to end_with('/tmp/cache') } - end - - describe '#work_dir' do - subject { uploader.work_dir } - - it { is_expected.to start_with(path) } - it { is_expected.to end_with('/tmp/work') } - end + it_behaves_like "builds correct paths", + store_dir: %r[\h{2}/\h{2}], + cache_dir: %r[/lfs-objects/tmp/cache], + work_dir: %r[/lfs-objects/tmp/work] end diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb index c6c4500c179..24a2fc0f72e 100644 --- a/spec/uploaders/namespace_file_uploader_spec.rb +++ b/spec/uploaders/namespace_file_uploader_spec.rb @@ -1,21 +1,16 @@ require 'spec_helper' +IDENTIFIER = %r{\h+/\S+} + describe NamespaceFileUploader do let(:group) { build_stubbed(:group) } let(:uploader) { described_class.new(group) } + let(:upload) { create(:upload, :namespace_upload, model: group) } - describe "#store_dir" do - it "stores in the namespace id directory" do - expect(uploader.store_dir).to include(group.id.to_s) - end - end - - describe ".absolute_path" do - it "stores in thecorrect directory" do - upload_record = create(:upload, :namespace_upload, model: group) + subject { uploader } - expect(described_class.absolute_path(upload_record)) - .to include("-/system/namespace/#{group.id}") - end - end + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/namespace/\d+], + upload_path: IDENTIFIER, + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}] end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index cbafa9f478d..ed1fba6edda 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -1,25 +1,27 @@ require 'spec_helper' +IDENTIFIER = %r{\h+/\S+} + describe PersonalFileUploader do - let(:uploader) { described_class.new(build_stubbed(:project)) } - let(:snippet) { create(:personal_snippet) } + let(:model) { create(:personal_snippet) } + let(:uploader) { described_class.new(model) } + let(:upload) { create(:upload, :personal_snippet_upload) } - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - upload = double(model: snippet, path: 'secret/foo.jpg') + subject { uploader } - dynamic_segment = "personal_snippet/#{snippet.id}" - - expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg") - end - end + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/personal_snippet/\d+], + upload_path: IDENTIFIER, + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}] describe '#to_h' do - it 'returns the hass' do - uploader = described_class.new(snippet, 'secret') + before do + subject.instance_variable_set(:@secret, 'secret') + end + it 'is correct' do allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name')) - expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name" + expected_url = "/uploads/-/system/personal_snippet/#{model.id}/secret/file_name" expect(uploader.to_h).to eq( alt: 'file_name', diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index 7ef7fb7d758..9a3e5d83e01 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -3,16 +3,16 @@ require 'rails_helper' describe RecordsUploads do let!(:uploader) do class RecordsUploadsExampleUploader < GitlabUploader - include RecordsUploads + include RecordsUploads::Concern storage :file - def model - FactoryBot.build_stubbed(:user) + def dynamic_segment + 'co/fe/ee' end end - RecordsUploadsExampleUploader.new + RecordsUploadsExampleUploader.new(build_stubbed(:user)) end def upload_fixture(filename) @@ -20,48 +20,55 @@ describe RecordsUploads do end describe 'callbacks' do - it 'calls `record_upload` after `store`' do + let(:upload) { create(:upload) } + + before do + uploader.upload = upload + end + + it '#record_upload after `store`' do expect(uploader).to receive(:record_upload).once uploader.store!(upload_fixture('doc_sample.txt')) end - it 'calls `destroy_upload` after `remove`' do - expect(uploader).to receive(:destroy_upload).once - + it '#destroy_upload after `remove`' do uploader.store!(upload_fixture('doc_sample.txt')) + expect(uploader).to receive(:destroy_upload).once uploader.remove! end end describe '#record_upload callback' do - it 'returns early when not using file storage' do - allow(uploader).to receive(:file_storage?).and_return(false) - expect(Upload).not_to receive(:record) - - uploader.store!(upload_fixture('rails_sample.jpg')) + it 'creates an Upload record after store' do + expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.to change { Upload.count }.by(1) end - it "returns early when the file doesn't exist" do - allow(uploader).to receive(:file).and_return(double(exists?: false)) - expect(Upload).not_to receive(:record) - + it 'creates a new record and assigns size, path, model, and uploader' do uploader.store!(upload_fixture('rails_sample.jpg')) + + upload = uploader.upload + aggregate_failures do + expect(upload).to be_persisted + expect(upload.size).to eq uploader.file.size + expect(upload.path).to eq uploader.upload_path + expect(upload.model_id).to eq uploader.model.id + expect(upload.model_type).to eq uploader.model.class.to_s + expect(upload.uploader).to eq uploader.class.to_s + end end - it 'creates an Upload record after store' do - expect(Upload).to receive(:record) - .with(uploader) + it "does not create an Upload record when the file doesn't exist" do + allow(uploader).to receive(:file).and_return(double(exists?: false)) - uploader.store!(upload_fixture('rails_sample.jpg')) + expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count } end it 'does not create an Upload record if model is missing' do - expect_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil) - expect(Upload).not_to receive(:record).with(uploader) + allow_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil) - uploader.store!(upload_fixture('rails_sample.jpg')) + expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count } end it 'it destroys Upload records at the same path before recording' do @@ -72,29 +79,15 @@ describe RecordsUploads do uploader: uploader.class.to_s ) + uploader.upload = existing uploader.store!(upload_fixture('rails_sample.jpg')) expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect(Upload.count).to eq 1 + expect(Upload.count).to eq(1) end end describe '#destroy_upload callback' do - it 'returns early when not using file storage' do - uploader.store!(upload_fixture('rails_sample.jpg')) - - allow(uploader).to receive(:file_storage?).and_return(false) - expect(Upload).not_to receive(:remove_path) - - uploader.remove! - end - - it 'returns early when file is nil' do - expect(Upload).not_to receive(:remove_path) - - uploader.remove! - end - it 'it destroys Upload records at the same path after removal' do uploader.store!(upload_fixture('rails_sample.jpg')) diff --git a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb new file mode 100644 index 00000000000..d0e692635b9 --- /dev/null +++ b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'projects/buttons/_dropdown' do + let(:user) { create(:user) } + + context 'user with all abilities' do + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:can?).and_return(true) + end + + context 'empty repository' do + let(:project) { create(:project, :empty_repo) } + + it 'has a link to create a new file' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).to have_link('New file') + end + + it 'does not have a link to create a new branch' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).not_to have_link('New branch') + end + + it 'does not have a link to create a new tag' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).not_to have_link('New tag') + end + end + end +end diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb index 9b293065797..ec435ec3b32 100644 --- a/spec/views/projects/imports/new.html.haml_spec.rb +++ b/spec/views/projects/imports/new.html.haml_spec.rb @@ -8,7 +8,7 @@ describe "projects/imports/new.html.haml" do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end it "escapes HTML in import errors" do diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 28d54c2fb77..264e0ce0b40 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -54,6 +54,8 @@ describe 'projects/merge_requests/show.html.haml' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') forked_project.destroy + # Reload merge request so MergeRequest#source_project turns to `nil` + closed_merge_request.reload render diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb new file mode 100644 index 00000000000..6e7d8db99c4 --- /dev/null +++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'projects/pipeline_schedules/_pipeline_schedule' do + let(:owner) { create(:user) } + let(:master) { create(:user) } + let(:project) { create(:project) } + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) } + + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule) + + allow(view).to receive(:can?).and_return(true) + end + + context 'taking ownership of schedule' do + context 'when non-owner is signed in' do + let(:user) { master } + + before do + allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true) + end + + it 'non-owner can take ownership of pipeline' do + render + + expect(rendered).to have_link('Take ownership') + end + end + + context 'when owner is signed in' do + let(:user) { owner } + + before do + allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false) + end + + it 'owner cannot take ownership of pipeline' do + render + + expect(rendered).not_to have_link('Take ownership') + end + end + end +end diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb index 95f0be49412..7b300150874 100644 --- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb +++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb @@ -13,8 +13,8 @@ describe 'projects/pipelines_settings/_show' do render expect(rendered).to have_css('.settings-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the') - expect(rendered).to have_link('Kubernetes service') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a') + expect(rendered).to have_link('Kubernetes cluster') end end @@ -27,8 +27,8 @@ describe 'projects/pipelines_settings/_show' do render expect(rendered).to have_css('.settings-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the') - expect(rendered).to have_link('Kubernetes service') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a') + expect(rendered).to have_link('Kubernetes cluster') end end end diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb index cae6bee2776..50980718e66 100644 --- a/spec/views/shared/notes/_form.html.haml_spec.rb +++ b/spec/views/shared/notes/_form.html.haml_spec.rb @@ -7,7 +7,7 @@ describe 'shared/notes/_form' do let(:project) { create(:project, :repository) } before do - project.team << [user, :master] + project.add_master(user) assign(:project, project) assign(:note, note) diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb index 1c54cf55fa0..d67e7698635 100644 --- a/spec/workers/background_migration_worker_spec.rb +++ b/spec/workers/background_migration_worker_spec.rb @@ -1,13 +1,32 @@ require 'spec_helper' -describe BackgroundMigrationWorker, :sidekiq do +describe BackgroundMigrationWorker, :sidekiq, :clean_gitlab_redis_shared_state do + let(:worker) { described_class.new } + describe '.perform' do it 'performs a background migration' do expect(Gitlab::BackgroundMigration) .to receive(:perform) .with('Foo', [10, 20]) - described_class.new.perform('Foo', [10, 20]) + worker.perform('Foo', [10, 20]) + end + + it 'reschedules a migration if it was performed recently' do + expect(worker) + .to receive(:always_perform?) + .and_return(false) + + worker.lease_for('Foo').try_obtain + + expect(Gitlab::BackgroundMigration) + .not_to receive(:perform) + + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) end end end diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb new file mode 100644 index 00000000000..7b7a7c1bc44 --- /dev/null +++ b/spec/workers/check_gcp_project_billing_worker_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe CheckGcpProjectBillingWorker do + describe '.perform' do + let(:token) { 'bogustoken' } + + subject { described_class.new.perform('token_key') } + + context 'when there is a token in redis' do + before do + allow(described_class).to receive(:get_session_token).and_return(token) + end + + context 'when there is no lease' do + before do + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') + end + + it 'calls the service' do + expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + + subject + end + + it 'stores billing status in redis' do + redis_double = double + + expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) + expect(redis_double).to receive(:set).with(described_class.redis_shared_state_key_for(token), anything, anything) + + subject + end + end + + context 'when there is a lease' do + before do + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false) + end + + it 'does not call the service' do + expect(CheckGcpProjectBillingService).not_to receive(:new) + + subject + end + end + end + + context 'when there is no token in redis' do + before do + allow_any_instance_of(described_class).to receive(:get_session_token).and_return(nil) + end + + it 'does not call the service' do + expect(CheckGcpProjectBillingService).not_to receive(:new) + + subject + end + end + end +end diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb new file mode 100644 index 00000000000..6b222af454d --- /dev/null +++ b/spec/workers/gitlab_shell_worker_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe GitlabShellWorker do + let(:worker) { described_class.new } + + describe '#perform with add_key' do + it 'calls add_key on Gitlab::Shell' do + expect_any_instance_of(Gitlab::Shell).to receive(:add_key).with('foo', 'bar') + worker.perform(:add_key, 'foo', 'bar') + end + end +end diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index 303193bab9b..c861a56497e 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -8,7 +8,7 @@ describe MergeWorker do let!(:author) { merge_request.author } before do - source_project.team << [author, :master] + source_project.add_master(author) source_project.repository.expire_branches_cache end diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb index 4e15ccc534b..baa8ddb59e5 100644 --- a/spec/workers/new_issue_worker_spec.rb +++ b/spec/workers/new_issue_worker_spec.rb @@ -44,8 +44,9 @@ describe NewIssueWorker do expect { worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1) end - it 'creates a notification for the assignee' do - expect(Notify).to receive(:new_issue_email).with(mentioned.id, issue.id).and_return(double(deliver_later: true)) + it 'creates a notification for the mentioned user' do + expect(Notify).to receive(:new_issue_email).with(mentioned.id, issue.id, NotificationReason::MENTIONED) + .and_return(double(deliver_later: true)) worker.perform(issue.id, user.id) end diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb index 9e0cbde45b1..c3f29a40d58 100644 --- a/spec/workers/new_merge_request_worker_spec.rb +++ b/spec/workers/new_merge_request_worker_spec.rb @@ -46,8 +46,10 @@ describe NewMergeRequestWorker do expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1) end - it 'creates a notification for the assignee' do - expect(Notify).to receive(:new_merge_request_email).with(mentioned.id, merge_request.id).and_return(double(deliver_later: true)) + it 'creates a notification for the mentioned user' do + expect(Notify).to receive(:new_merge_request_email) + .with(mentioned.id, merge_request.id, NotificationReason::MENTIONED) + .and_return(double(deliver_later: true)) worker.perform(merge_request.id, user.id) end diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb new file mode 100644 index 00000000000..20aff020dbb --- /dev/null +++ b/spec/workers/rebase_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe RebaseWorker, '#perform' do + context 'when rebasing an MR from a fork where upstream has protected branches' do + let(:upstream_project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository) } + + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + source_branch: 'feature_conflict', + target_project: upstream_project, + target_branch: 'master') + end + + before do + create(:forked_project_link, forked_to_project: fork_project, forked_from_project: upstream_project) + end + + it 'sets the correct project for running hooks' do + expect(MergeRequests::RebaseService) + .to receive(:new).with(fork_project, merge_request.author).and_call_original + + subject.perform(merge_request, merge_request.author) + end + end +end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 31598586f59..4912baa348c 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -47,6 +47,14 @@ describe RepositoryForkWorker do perform! end + it 'protects the default branch' do + expect_fork_repository.and_return(true) + + perform! + + expect(fork_project.protected_branches.first.name).to eq(fork_project.default_branch) + end + it 'flushes various caches' do expect_fork_repository.and_return(true) diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 85ac14eb347..2b1a617ee62 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -32,6 +32,7 @@ describe RepositoryImportWorker do expect_any_instance_of(Projects::ImportService).to receive(:execute) .and_return({ status: :ok }) + expect_any_instance_of(Project).to receive(:after_import).and_call_original expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) expect_any_instance_of(Project).to receive(:import_finish) @@ -48,9 +49,22 @@ describe RepositoryImportWorker do expect do subject.perform(project.id) - end.to raise_error(StandardError, error) + end.to raise_error(RuntimeError, error) expect(project.reload.import_jid).not_to be_nil end + + it 'updates the error on Import/Export' do + error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + + project.update_attributes(import_jid: '123', import_type: 'gitlab_project') + expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) + + expect do + subject.perform(project.id) + end.to raise_error(RuntimeError, error) + + expect(project.reload.import_error).not_to be_nil + end end context 'when using an asynchronous importer' do diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb index 911360da66c..9e50ce15871 100644 --- a/spec/workers/upload_checksum_worker_spec.rb +++ b/spec/workers/upload_checksum_worker_spec.rb @@ -2,18 +2,31 @@ require 'rails_helper' describe UploadChecksumWorker do describe '#perform' do - it 'rescues ActiveRecord::RecordNotFound' do - expect { described_class.new.perform(999_999) }.not_to raise_error + subject { described_class.new } + + context 'without a valid record' do + it 'rescues ActiveRecord::RecordNotFound' do + expect { subject.perform(999_999) }.not_to raise_error + end end - it 'calls calculate_checksum_without_delay and save!' do - upload = spy - expect(Upload).to receive(:find).with(999_999).and_return(upload) + context 'with a valid record' do + let(:upload) { create(:user, :with_avatar).avatar.upload } + + before do + expect(Upload).to receive(:find).and_return(upload) + allow(upload).to receive(:foreground_checksumable?).and_return(false) + end - described_class.new.perform(999_999) + it 'calls calculate_checksum!' do + expect(upload).to receive(:calculate_checksum!) + subject.perform(upload.id) + end - expect(upload).to have_received(:calculate_checksum) - expect(upload).to have_received(:save!) + it 'calls save!' do + expect(upload).to receive(:save!) + subject.perform(upload.id) + end end end end |