diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-05-30 08:50:09 +0300 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-05-30 08:50:09 +0300 |
commit | 09122f93c34b15cb827aabdbdf35fc33b08f93af (patch) | |
tree | 57c137ef57621a7a2ed4940c56c7f5cbe6ec1c80 /spec | |
parent | 1d20679e9c8b1ba16bebaf982255946e7207b4d4 (diff) | |
parent | 5b1416aa74c4fa80e0c324fd2907166af5ca479b (diff) |
Merge branch 'master' into per-project-pipeline-iid
Diffstat (limited to 'spec')
73 files changed, 1890 insertions, 591 deletions
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 4770e187db6..dcb0faffbd4 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -17,7 +17,7 @@ describe Boards::IssuesController do project.add_guest(guest) end - describe 'GET index' do + describe 'GET index', :request_store do let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } context 'with invalid board id' do diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index c621eb69171..4530a301d4d 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -3,6 +3,19 @@ require('spec_helper') describe ProfilesController, :request_store do let(:user) { create(:user) } + describe 'POST update' do + it 'does not update password' do + sign_in(user) + + expect do + post :update, + user: { password: 'hello12345', password_confirmation: 'hello12345' } + end.not_to change { user.reload.encrypted_password } + + expect(response.status).to eq(302) + end + end + describe 'PUT update' do it 'allows an email update from a user without an external email address' do sign_in(user) diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 4d765229bde..509f19ed030 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -27,6 +27,20 @@ describe Projects::BoardsController do expect(response).to render_template :index expect(response.content_type).to eq 'text/html' end + + context 'with unauthorized user' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) + end + + it 'returns a not found 404 response' do + list_boards + + expect(response).to have_gitlab_http_status(404) + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do @@ -40,18 +54,19 @@ describe Projects::BoardsController do expect(response).to match_response_schema('boards') expect(parsed_response.length).to eq 2 end - end - context 'with unauthorized user' do - before do - allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) - end + context 'with unauthorized user' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) + end - it 'returns a not found 404 response' do - list_boards + it 'returns a not found 404 response' do + list_boards format: :json - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(404) + expect(response.content_type).to eq 'application/json' + end end end @@ -88,6 +103,20 @@ describe Projects::BoardsController do expect(response).to render_template :show expect(response.content_type).to eq 'text/html' end + + context 'with unauthorized user' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) + end + + it 'returns a not found 404 response' do + read_board board: board + + expect(response).to have_gitlab_http_status(404) + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do @@ -96,18 +125,19 @@ describe Projects::BoardsController do expect(response).to match_response_schema('board') end - end - context 'with unauthorized user' do - before do - allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) - end + context 'with unauthorized user' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) + end - it 'returns a not found 404 response' do - read_board board: board + it 'returns a not found 404 response' do + read_board board: board, format: :json - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(404) + expect(response.content_type).to eq 'application/json' + end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 16fb377b002..4860ea5dcce 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -146,6 +146,24 @@ describe Projects::BranchesController do it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' end + + it 'redirects to autodeploy setup page' do + result = { status: :success, branch: double(name: branch) } + + create(:cluster, :provided_by_gcp, projects: [project]) + + expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + + expect(response.location).to include(project_new_blob_path(project, branch)) + expect(response).to have_gitlab_http_status(302) + end end context 'when create branch service fails' do diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 715bb9f5e52..271ba37aed4 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -77,8 +77,6 @@ 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) @@ -137,33 +135,15 @@ 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 google project billing is enabled' do - before do - redis_double = double.as_null_object - 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 } - .and change { Clusters::Providers::Gcp.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - 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.now[:alert] - expect(response).to render_template('new') - end + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes end end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 82b20e12850..380e50c8cac 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe Projects::ClustersController do include AccessMatchersForController - include GoogleApi::CloudPlatformHelpers set(:project) { create(:project) } @@ -333,7 +332,7 @@ describe Projects::ClustersController do context 'when cluster is provided by GCP' do context 'when cluster is created' do - let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } it "destroys and redirects back to clusters list" do expect { go } @@ -347,7 +346,7 @@ describe Projects::ClustersController do end context 'when cluster is being created' do - let!(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } + let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, projects: [project]) } it "destroys and redirects back to clusters list" do expect { go } @@ -361,7 +360,7 @@ describe Projects::ClustersController do end context 'when cluster is provided by user' do - let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) } + let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, projects: [project]) } it "destroys and redirects back to clusters list" do expect { go } @@ -376,7 +375,7 @@ describe Projects::ClustersController do end describe 'security' do - set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index ff9ab53d8c3..47d4942acbd 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -21,6 +21,13 @@ describe Projects::EnvironmentsController do expect(response).to have_gitlab_http_status(:ok) end + + it 'expires etag cache to force reload environments list' do + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch).with(project_environments_path(project, format: :json)) + + get :index, environment_params + end end context 'when requesting JSON response for folders' do diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 5bfc3d31401..72f6af112b3 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -21,6 +21,18 @@ describe Projects::GroupLinksController do end end + context 'when project is not allowed to be shared with a group' do + before do + group.update_attributes(share_with_group_lock: false) + end + + include_context 'link project to group' + + it 'responds with status 404' do + expect(response).to have_gitlab_http_status(404) + end + end + context 'when user has access to group he want to link project to' do before do group.add_developer(user) diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 7fb4c1b7425..011843baffc 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -2,16 +2,15 @@ require 'spec_helper' describe Projects::ImportsController do let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + sign_in(user) + project.add_master(user) + end describe 'GET #show' do context 'when repository does not exists' do - let(:project) { create(:project) } - - before do - sign_in(user) - project.add_master(user) - end - it 'renders template' do get :show, namespace_id: project.namespace.to_param, project_id: project @@ -28,11 +27,6 @@ describe Projects::ImportsController do context 'when repository exists' do let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') } - before do - sign_in(user) - project.add_master(user) - end - context 'when import is in progress' do before do project.update_attribute(:import_status, :started) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ca86b0bc737..106611b37c9 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1,4 +1,4 @@ -require('spec_helper') +require 'spec_helper' describe Projects::IssuesController do let(:project) { create(:project) } diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 46b08a03b19..d84b31ad978 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -184,7 +184,7 @@ describe Projects::ProjectMembersController do project.add_master(user) end - it 'cannot remove himself from the project' do + it 'cannot remove themselves from the project' do delete :leave, namespace_id: project.namespace, project_id: project diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb index 3ecc90b6573..00c063c49f8 100644 --- a/spec/factories/application_settings.rb +++ b/spec/factories/application_settings.rb @@ -1,4 +1,5 @@ FactoryBot.define do factory :application_setting do + default_projects_limit 42 end end diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 1ce30015e81..bf329b0bb94 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -83,7 +83,7 @@ feature 'Edit group settings' do attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) - expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true) + expect { save_group }.to change { group.reload.avatar? }.to(true) end it 'uploads new group avatar' do @@ -97,10 +97,19 @@ feature 'Edit group settings' do expect(page).not_to have_link('Remove avatar') end end -end -def update_path(new_group_path) - visit edit_group_path(group) - fill_in 'group_path', with: new_group_path - click_button 'Save group' + def update_path(new_group_path) + visit edit_group_path(group) + + page.within('.gs-advanced') do + fill_in 'group_path', with: new_group_path + click_button 'Change group path' + end + end + + def save_group + page.within('.gs-general') do + click_button 'Save group' + end + end end diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb index 8842d1391aa..cefbc15e068 100644 --- a/spec/features/groups/share_lock_spec.rb +++ b/spec/features/groups/share_lock_spec.rb @@ -15,9 +15,8 @@ feature 'Group share with group lock' do context 'when enabling the parent group share with group lock' do scenario 'the subgroup share with group lock becomes enabled' do visit edit_group_path(root_group) - check 'group_share_with_group_lock' - click_on 'Save group' + enable_group_lock expect(subgroup.reload.share_with_group_lock?).to be_truthy end @@ -26,16 +25,15 @@ feature 'Group share with group lock' do context 'when disabling the parent group share with group lock (which was already enabled)' do background do visit edit_group_path(root_group) - check 'group_share_with_group_lock' - click_on 'Save group' + + enable_group_lock end context 'and the subgroup share with group lock is enabled' do scenario 'the subgroup share with group lock does not change' do visit edit_group_path(root_group) - uncheck 'group_share_with_group_lock' - click_on 'Save group' + disable_group_lock expect(subgroup.reload.share_with_group_lock?).to be_truthy end @@ -44,19 +42,32 @@ feature 'Group share with group lock' do context 'but the subgroup share with group lock is disabled' do background do visit edit_group_path(subgroup) - uncheck 'group_share_with_group_lock' - click_on 'Save group' + + disable_group_lock end scenario 'the subgroup share with group lock does not change' do visit edit_group_path(root_group) - uncheck 'group_share_with_group_lock' - click_on 'Save group' + disable_group_lock expect(subgroup.reload.share_with_group_lock?).to be_falsey end end end end + + def enable_group_lock + page.within('.gs-permissions') do + check 'group_share_with_group_lock' + click_on 'Save group' + end + end + + def disable_group_lock + page.within('.gs-permissions') do + uncheck 'group_share_with_group_lock' + click_on 'Save group' + end + end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index c1f3d94bc20..236768b5d7f 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -141,8 +141,10 @@ feature 'Group' do end it 'saves new settings' do - fill_in 'group_name', with: new_name - click_button 'Save group' + page.within('.gs-general') do + fill_in 'group_name', with: new_name + click_button 'Save group' + end expect(page).to have_content 'successfully updated' expect(find('#group_name').value).to eq(new_name) diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index a8a627d8806..c85b82b2090 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -22,152 +22,123 @@ feature 'Gcp Cluster', :js do .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end - context 'when user has a GCP project with billing enabled' do + context 'when user does not have a cluster and visits cluster index page' 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(true) - end - - context 'when user does not have a cluster and visits cluster index page' do - before do - visit project_clusters_path(project) - - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' - 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 Kubernetes cluster' - end + visit project_clusters_path(project) - it 'user sees a cluster details page and creation status' do - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + click_link 'Add Kubernetes cluster' + click_link 'Create on Google Kubernetes Engine' + end - Clusters::Cluster.last.provider.make_created! + context 'when user filled form with valid parameters' do + subject { click_button 'Create Kubernetes cluster' } - expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') + 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 - it 'user sees a error if something worng during creation' do - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) - Clusters::Cluster.last.provider.make_errored!('Something wrong!') + execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")') + sleep 2 # wait for ajax + execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")') + execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")') + execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")') - expect(page).to have_content('Something wrong!') - end + fill_in 'cluster[name]', with: 'dev-cluster' + fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123' + fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a' + fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2' end - context 'when user filled form with invalid parameters' do - before do - click_button 'Create Kubernetes cluster' - end - - it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') - end + it 'users sees a form with the GCP token' do + expect(page).to have_selector(:css, 'form[data-token="token"]') end - end - context 'when user does have a cluster and visits cluster page' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + it 'user sees a cluster details page and creation status' do + subject - before do - visit project_cluster_path(project, cluster) - end + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + + Clusters::Cluster.last.provider.make_created! - 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) + expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') end - 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 + it 'user sees a error if something wrong during creation' do + subject - it 'user sees the successful message' do - expect(page).to have_content('Kubernetes cluster was successfully updated.') - end - end + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') - 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 + Clusters::Cluster.last.provider.make_errored!('Something wrong!') - it 'user sees the successful message' do - expect(page).to have_content('Kubernetes cluster was successfully updated.') - expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') - end + expect(page).to have_content('Something wrong!') end + end - context 'when user destroy the cluster' do - before do - page.accept_confirm do - click_link 'Remove integration' - end - end + context 'when user filled form with invalid parameters' do + before do + execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")') + click_button 'Create Kubernetes cluster' + end - it 'user sees creation form with the successful message' do - expect(page).to have_content('Kubernetes cluster integration was successfully removed.') - expect(page).to have_link('Add Kubernetes cluster') - end + it 'user sees a validation error' do + expect(page).to have_css('#error_explanation') end end 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) - - visit project_clusters_path(project) - - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + context 'when user does have a cluster and visits cluster page' do + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create Kubernetes cluster' + before do + visit project_cluster_path(project, cluster) 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 Kubernetes cluster, then try again.') + 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 - 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) + 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 - visit project_clusters_path(project) + it 'user sees the successful message' do + expect(page).to have_content('Kubernetes cluster was successfully updated.') + end + end - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + 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 - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create Kubernetes cluster' + it 'user sees the successful message' do + expect(page).to have_content('Kubernetes cluster was successfully updated.') + expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') + end 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.') + 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('Kubernetes cluster integration was successfully removed.') + expect(page).to have_link('Add Kubernetes cluster') + end end end end diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb index 295eb02b625..d36aafdbc54 100644 --- a/spec/features/projects/merge_requests/user_views_diffs_spec.rb +++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb @@ -26,6 +26,10 @@ describe 'User views diffs', :js do expect(page).to have_css('#inline-diff-btn', count: 1) end + it 'hides loading spinner after load' do + expect(page).not_to have_selector('.mr-loading-status .loading', visible: true) + end + context 'when in the inline view' do include_examples 'unfold diffs' end diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb index 8a9255db9e8..ee5734a9bf1 100644 --- a/spec/features/projects/user_sees_sidebar_spec.rb +++ b/spec/features/projects/user_sees_sidebar_spec.rb @@ -44,6 +44,18 @@ describe 'Projects > User sees sidebar' do expect(page).not_to have_content 'Repository' expect(page).not_to have_content 'CI / CD' expect(page).not_to have_content 'Merge Requests' + expect(page).not_to have_content 'Operations' + end + end + + it 'shows build tab if builds are public' do + project.public_builds = true + project.save + + visit project_path(project) + + within('.nav-sidebar') do + expect(page).to have_content 'CI / CD' end end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index e473739a6aa..bbdd98a7623 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -19,6 +19,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do visit project_path(project) find('.shortcuts-wiki').click + click_link "Create your first page" end context "while creating a new wiki page" do diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 9989e1ffda7..706894f4b32 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -8,6 +8,7 @@ describe "User creates wiki page" do sign_in(user) visit(project_wikis_path(project)) + click_link "Create your first page" end context "when wiki is empty" do 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 e019e3ce5a5..272dac127dd 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -11,6 +11,7 @@ describe 'User updates wiki page' do context 'when wiki is empty' do before do visit(project_wikis_path(project)) + click_link "Create your first page" end context 'in a user namespace' do diff --git a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb new file mode 100644 index 00000000000..83ffbb4a94e --- /dev/null +++ b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe 'User views empty wiki' do + let(:user) { create(:user) } + + shared_examples 'empty wiki and accessible issues' do + it 'show "issue tracker" message' do + visit(project_wikis_path(project)) + + element = page.find('.row.empty-state') + + expect(element).to have_content('This project has no wiki pages') + expect(element).to have_link("issue tracker", href: project_issues_path(project)) + expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project)) + end + end + + shared_examples 'empty wiki and non-accessible issues' do + it 'does not show "issue tracker" message' do + visit(project_wikis_path(project)) + + element = page.find('.row.empty-state') + + expect(element).to have_content('This project has no wiki pages') + expect(element).to have_no_link('Suggest wiki improvement') + end + end + + context 'when user is logged out and issue tracker is public' do + let(:project) { create(:project, :public, :wiki_repo) } + + it_behaves_like 'empty wiki and accessible issues' + end + + context 'when user is logged in and not a member' do + let(:project) { create(:project, :public, :wiki_repo) } + + before do + sign_in(user) + end + + it_behaves_like 'empty wiki and accessible issues' + end + + context 'when issue tracker is private' do + let(:project) { create(:project, :public, :wiki_repo, :issues_private) } + + it_behaves_like 'empty wiki and non-accessible issues' + end + + context 'when issue tracker is disabled' do + let(:project) { create(:project, :public, :wiki_repo, :issues_disabled) } + + it_behaves_like 'empty wiki and non-accessible issues' + end + + context 'when user is logged in and a memeber' do + let(:project) { create(:project, :public, :wiki_repo) } + + before do + sign_in(user) + project.add_developer(user) + end + + it 'show "create first page" message' do + visit(project_wikis_path(project)) + + element = page.find('.row.empty-state') + + element.click_link 'Create your first page' + + expect(page).to have_button('Create page') + end + end +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 6661714222a..1de7d9a56a8 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -18,6 +18,7 @@ describe 'User views a wiki page' do context 'when wiki is empty' do before do visit(project_wikis_path(project)) + click_link "Create your first page" click_on('New page') @@ -140,6 +141,7 @@ describe 'User views a wiki page' do visit(project_path(project)) find('.shortcuts-wiki').click + click_link "Create your first page" expect(page).to have_content('Home · Create Page') end diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb index e8884bc1a00..c8db82a562f 100644 --- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -14,7 +14,9 @@ feature 'User uploads avatar to group' do visible: false ) - click_button 'Save group' + page.within('.gs-general') do + click_button 'Save group' + end visit group_path(group) diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb index 7bede0b0d48..5478e38ce70 100644 --- a/spec/features/users/user_browses_projects_on_user_page_spec.rb +++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb @@ -26,18 +26,23 @@ describe 'Users > User browses projects on user page', :js do end end + it 'hides loading spinner after load', :js do + visit user_path(user) + click_nav_link('Personal projects') + + wait_for_requests + + expect(page).not_to have_selector('.loading-status .loading', visible: true) + end + it 'paginates projects', :js do project = create(:project, namespace: user.namespace, updated_at: 2.minutes.since) project2 = create(:project, namespace: user.namespace, updated_at: 1.minute.since) allow(Project).to receive(:default_per_page).and_return(1) sign_in(user) - visit user_path(user) - - page.within('.user-profile-nav') do - click_link('Personal projects') - end + click_nav_link('Personal projects') wait_for_requests @@ -92,7 +97,6 @@ describe 'Users > User browses projects on user page', :js do 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) diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js new file mode 100644 index 00000000000..21805ef0b28 --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js @@ -0,0 +1,103 @@ +import Vue from 'vue'; +import GkeMachineTypeDropdown from '~/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue'; +import { createStore } from '~/projects/gke_cluster_dropdowns/store'; +import { + SET_PROJECT, + SET_PROJECT_BILLING_STATUS, + SET_ZONE, + SET_MACHINE_TYPES, +} from '~/projects/gke_cluster_dropdowns/store/mutation_types'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { + selectedZoneMock, + selectedProjectMock, + selectedMachineTypeMock, + gapiMachineTypesResponseMock, +} from '../mock_data'; + +const componentConfig = { + fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type', + fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]', +}; + +const LABELS = { + LOADING: 'Fetching machine types', + DISABLED_NO_PROJECT: 'Select project and zone to choose machine type', + DISABLED_NO_ZONE: 'Select zone to choose machine type', + DEFAULT: 'Select machine type', +}; + +const createComponent = (store, props = componentConfig) => { + const Component = Vue.extend(GkeMachineTypeDropdown); + + return mountComponentWithStore(Component, { + el: null, + props, + store, + }); +}; + +describe('GkeMachineTypeDropdown', () => { + let vm; + let store; + + beforeEach(() => { + store = createStore(); + vm = createComponent(store); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('shows various toggle text depending on state', () => { + it('returns disabled state toggle text when no project and zone are selected', () => { + expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT); + }); + + it('returns disabled state toggle text when no zone is selected', () => { + vm.$store.commit(SET_PROJECT, selectedProjectMock); + vm.$store.commit(SET_PROJECT_BILLING_STATUS, true); + + expect(vm.toggleText).toBe(LABELS.DISABLED_NO_ZONE); + }); + + it('returns loading toggle text', () => { + vm.isLoading = true; + + expect(vm.toggleText).toBe(LABELS.LOADING); + }); + + it('returns default toggle text', () => { + expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT); + + vm.$store.commit(SET_PROJECT, selectedProjectMock); + vm.$store.commit(SET_PROJECT_BILLING_STATUS, true); + vm.$store.commit(SET_ZONE, selectedZoneMock); + + expect(vm.toggleText).toBe(LABELS.DEFAULT); + }); + + it('returns machine type name if machine type selected', () => { + vm.setItem(selectedMachineTypeMock); + + expect(vm.toggleText).toBe(selectedMachineTypeMock); + }); + }); + + describe('form input', () => { + it('reflects new value when dropdown item is clicked', done => { + expect(vm.$el.querySelector('input').value).toBe(''); + vm.$store.commit(SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items); + + return vm.$nextTick().then(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('input').value).toBe(selectedMachineTypeMock); + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js new file mode 100644 index 00000000000..d4fcb2dc8ff --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js @@ -0,0 +1,92 @@ +import Vue from 'vue'; +import GkeProjectIdDropdown from '~/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue'; +import { createStore } from '~/projects/gke_cluster_dropdowns/store'; +import { SET_PROJECTS } from '~/projects/gke_cluster_dropdowns/store/mutation_types'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { emptyProjectMock, selectedProjectMock } from '../mock_data'; + +const componentConfig = { + docsUrl: 'https://console.cloud.google.com/home/dashboard', + fieldId: 'cluster_provider_gcp_attributes_gcp_project_id', + fieldName: 'cluster[provider_gcp_attributes][gcp_project_id]', +}; + +const LABELS = { + LOADING: 'Fetching projects', + VALIDATING_PROJECT_BILLING: 'Validating project billing status', + DEFAULT: 'Select project', + EMPTY: 'No projects found', +}; + +const createComponent = (store, props = componentConfig) => { + const Component = Vue.extend(GkeProjectIdDropdown); + + return mountComponentWithStore(Component, { + el: null, + props, + store, + }); +}; + +describe('GkeProjectIdDropdown', () => { + let vm; + let store; + + beforeEach(() => { + store = createStore(); + vm = createComponent(store); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggleText', () => { + it('returns loading toggle text', () => { + expect(vm.toggleText).toBe(LABELS.LOADING); + }); + + it('returns project billing validation text', () => { + vm.setIsValidatingProjectBilling(true); + expect(vm.toggleText).toBe(LABELS.VALIDATING_PROJECT_BILLING); + }); + + it('returns default toggle text', done => + vm.$nextTick().then(() => { + vm.setItem(emptyProjectMock); + + expect(vm.toggleText).toBe(LABELS.DEFAULT); + done(); + })); + + it('returns project name if project selected', done => + vm.$nextTick().then(() => { + expect(vm.toggleText).toBe(selectedProjectMock.name); + done(); + })); + + it('returns empty toggle text', done => + vm.$nextTick().then(() => { + vm.$store.commit(SET_PROJECTS, null); + vm.setItem(emptyProjectMock); + + expect(vm.toggleText).toBe(LABELS.EMPTY); + done(); + })); + }); + + describe('selectItem', () => { + it('reflects new value when dropdown item is clicked', done => { + expect(vm.$el.querySelector('input').value).toBe(''); + + return vm.$nextTick().then(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('input').value).toBe(selectedProjectMock.projectId); + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js new file mode 100644 index 00000000000..89a4a7ea2ce --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import GkeZoneDropdown from '~/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue'; +import { createStore } from '~/projects/gke_cluster_dropdowns/store'; +import { + SET_PROJECT, + SET_ZONES, + SET_PROJECT_BILLING_STATUS, +} from '~/projects/gke_cluster_dropdowns/store/mutation_types'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data'; + +const componentConfig = { + fieldId: 'cluster_provider_gcp_attributes_gcp_zone', + fieldName: 'cluster[provider_gcp_attributes][gcp_zone]', +}; + +const LABELS = { + LOADING: 'Fetching zones', + DISABLED: 'Select project to choose zone', + DEFAULT: 'Select zone', +}; + +const createComponent = (store, props = componentConfig) => { + const Component = Vue.extend(GkeZoneDropdown); + + return mountComponentWithStore(Component, { + el: null, + props, + store, + }); +}; + +describe('GkeZoneDropdown', () => { + let vm; + let store; + + beforeEach(() => { + store = createStore(); + vm = createComponent(store); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggleText', () => { + it('returns disabled state toggle text', () => { + expect(vm.toggleText).toBe(LABELS.DISABLED); + }); + + it('returns loading toggle text', () => { + vm.isLoading = true; + + expect(vm.toggleText).toBe(LABELS.LOADING); + }); + + it('returns default toggle text', () => { + expect(vm.toggleText).toBe(LABELS.DISABLED); + + vm.$store.commit(SET_PROJECT, selectedProjectMock); + vm.$store.commit(SET_PROJECT_BILLING_STATUS, true); + + expect(vm.toggleText).toBe(LABELS.DEFAULT); + }); + + it('returns project name if project selected', () => { + vm.setItem(selectedZoneMock); + + expect(vm.toggleText).toBe(selectedZoneMock); + }); + }); + + describe('selectItem', () => { + it('reflects new value when dropdown item is clicked', done => { + expect(vm.$el.querySelector('input').value).toBe(''); + vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items); + + return vm.$nextTick().then(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('input').value).toBe(selectedZoneMock); + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js new file mode 100644 index 00000000000..6df511e9157 --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js @@ -0,0 +1,49 @@ +import { + gapiProjectsResponseMock, + gapiZonesResponseMock, + gapiMachineTypesResponseMock, +} from './mock_data'; + +// eslint-disable-next-line import/prefer-default-export +export const gapi = () => ({ + client: { + cloudbilling: { + projects: { + getBillingInfo: () => + new Promise(resolve => { + resolve({ + result: { billingEnabled: true }, + }); + }), + }, + }, + cloudresourcemanager: { + projects: { + list: () => + new Promise(resolve => { + resolve({ + result: { ...gapiProjectsResponseMock }, + }); + }), + }, + }, + compute: { + zones: { + list: () => + new Promise(resolve => { + resolve({ + result: { ...gapiZonesResponseMock }, + }); + }), + }, + machineTypes: { + list: () => + new Promise(resolve => { + resolve({ + result: { ...gapiMachineTypesResponseMock }, + }); + }), + }, + }, + }, +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js new file mode 100644 index 00000000000..d9f5dbc636f --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js @@ -0,0 +1,75 @@ +export const emptyProjectMock = { + projectId: '', + name: '', +}; + +export const selectedProjectMock = { + projectId: 'gcp-project-123', + name: 'gcp-project', +}; + +export const selectedZoneMock = 'us-central1-a'; + +export const selectedMachineTypeMock = 'n1-standard-2'; + +export const gapiProjectsResponseMock = { + projects: [ + { + projectNumber: '1234', + projectId: 'gcp-project-123', + lifecycleState: 'ACTIVE', + name: 'gcp-project', + createTime: '2017-12-16T01:48:29.129Z', + parent: { + type: 'organization', + id: '12345', + }, + }, + ], +}; + +export const gapiZonesResponseMock = { + kind: 'compute#zoneList', + id: 'projects/gitlab-internal-153318/zones', + items: [ + { + kind: 'compute#zone', + id: '2000', + creationTimestamp: '1969-12-31T16:00:00.000-08:00', + name: 'us-central1-a', + description: 'us-central1-a', + status: 'UP', + region: + 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1', + selfLink: + 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a', + availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'], + }, + ], + selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones', +}; + +export const gapiMachineTypesResponseMock = { + kind: 'compute#machineTypeList', + id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes', + items: [ + { + kind: 'compute#machineType', + id: '3002', + creationTimestamp: '1969-12-31T16:00:00.000-08:00', + name: 'n1-standard-2', + description: '2 vCPUs, 7.5 GB RAM', + guestCpus: 2, + memoryMb: 7680, + imageSpaceGb: 10, + maximumPersistentDisks: 64, + maximumPersistentDisksSizeGb: '65536', + zone: 'us-central1-a', + selfLink: + 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2', + isSharedCpu: false, + }, + ], + selfLink: + 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes', +}; diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js new file mode 100644 index 00000000000..9d892b8185b --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js @@ -0,0 +1,131 @@ +import testAction from 'spec/helpers/vuex_action_helper'; +import * as actions from '~/projects/gke_cluster_dropdowns/store/actions'; +import { createStore } from '~/projects/gke_cluster_dropdowns/store'; +import { gapi } from '../helpers'; +import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data'; + +describe('GCP Cluster Dropdown Store Actions', () => { + let store; + + beforeEach(() => { + store = createStore(); + }); + + describe('setProject', () => { + it('should set project', done => { + testAction( + actions.setProject, + selectedProjectMock, + { selectedProject: {} }, + [{ type: 'SET_PROJECT', payload: selectedProjectMock }], + [], + done, + ); + }); + }); + + describe('setZone', () => { + it('should set zone', done => { + testAction( + actions.setZone, + selectedZoneMock, + { selectedZone: '' }, + [{ type: 'SET_ZONE', payload: selectedZoneMock }], + [], + done, + ); + }); + }); + + describe('setMachineType', () => { + it('should set machine type', done => { + testAction( + actions.setMachineType, + selectedMachineTypeMock, + { selectedMachineType: '' }, + [{ type: 'SET_MACHINE_TYPE', payload: selectedMachineTypeMock }], + [], + done, + ); + }); + }); + + describe('setIsValidatingProjectBilling', () => { + it('should set machine type', done => { + testAction( + actions.setIsValidatingProjectBilling, + true, + { isValidatingProjectBilling: null }, + [{ type: 'SET_IS_VALIDATING_PROJECT_BILLING', payload: true }], + [], + done, + ); + }); + }); + + describe('async fetch methods', () => { + window.gapi = gapi(); + + describe('fetchProjects', () => { + it('fetches projects from Google API', done => { + store + .dispatch('fetchProjects') + .then(() => { + expect(store.state.projects[0].projectId).toEqual(selectedProjectMock.projectId); + expect(store.state.projects[0].name).toEqual(selectedProjectMock.name); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('validateProjectBilling', () => { + it('checks project billing status from Google API', done => { + testAction( + actions.validateProjectBilling, + true, + { + selectedProject: selectedProjectMock, + selectedZone: '', + selectedMachineType: '', + projectHasBillingEnabled: null, + }, + [ + { type: 'SET_ZONE', payload: '' }, + { type: 'SET_MACHINE_TYPE', payload: '' }, + { type: 'SET_PROJECT_BILLING_STATUS', payload: true }, + ], + [{ type: 'setIsValidatingProjectBilling', payload: false }], + done, + ); + }); + }); + + describe('fetchZones', () => { + it('fetches zones from Google API', done => { + store + .dispatch('fetchZones') + .then(() => { + expect(store.state.zones[0].name).toEqual(selectedZoneMock); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('fetchMachineTypes', () => { + it('fetches machine types from Google API', done => { + store + .dispatch('fetchMachineTypes') + .then(() => { + expect(store.state.machineTypes[0].name).toEqual(selectedMachineTypeMock); + + done(); + }) + .catch(done.fail); + }); + }); + }); +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js new file mode 100644 index 00000000000..6f89158f807 --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js @@ -0,0 +1,65 @@ +import * as getters from '~/projects/gke_cluster_dropdowns/store/getters'; +import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data'; + +describe('GCP Cluster Dropdown Store Getters', () => { + let state; + + describe('valid states', () => { + beforeEach(() => { + state = { + selectedProject: selectedProjectMock, + selectedZone: selectedZoneMock, + selectedMachineType: selectedMachineTypeMock, + }; + }); + + describe('hasProject', () => { + it('should return true when project is selected', () => { + expect(getters.hasProject(state)).toEqual(true); + }); + }); + + describe('hasZone', () => { + it('should return true when zone is selected', () => { + expect(getters.hasZone(state)).toEqual(true); + }); + }); + + describe('hasMachineType', () => { + it('should return true when machine type is selected', () => { + expect(getters.hasMachineType(state)).toEqual(true); + }); + }); + }); + + describe('invalid states', () => { + beforeEach(() => { + state = { + selectedProject: { + projectId: '', + name: '', + }, + selectedZone: '', + selectedMachineType: '', + }; + }); + + describe('hasProject', () => { + it('should return false when project is not selected', () => { + expect(getters.hasProject(state)).toEqual(false); + }); + }); + + describe('hasZone', () => { + it('should return false when zone is not selected', () => { + expect(getters.hasZone(state)).toEqual(false); + }); + }); + + describe('hasMachineType', () => { + it('should return false when machine type is not selected', () => { + expect(getters.hasMachineType(state)).toEqual(false); + }); + }); + }); +}); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js new file mode 100644 index 00000000000..7f8c4f314e4 --- /dev/null +++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js @@ -0,0 +1,87 @@ +import { createStore } from '~/projects/gke_cluster_dropdowns/store'; +import * as types from '~/projects/gke_cluster_dropdowns/store/mutation_types'; +import { + selectedProjectMock, + selectedZoneMock, + selectedMachineTypeMock, + gapiProjectsResponseMock, + gapiZonesResponseMock, + gapiMachineTypesResponseMock, +} from '../mock_data'; + +describe('GCP Cluster Dropdown Store Mutations', () => { + let store; + + beforeEach(() => { + store = createStore(); + }); + + describe('SET_PROJECT', () => { + it('should set GCP project as selectedProject', () => { + const projectToSelect = gapiProjectsResponseMock.projects[0]; + + store.commit(types.SET_PROJECT, projectToSelect); + + expect(store.state.selectedProject.projectId).toEqual(selectedProjectMock.projectId); + expect(store.state.selectedProject.name).toEqual(selectedProjectMock.name); + }); + }); + + describe('SET_PROJECT_BILLING_STATUS', () => { + it('should set project billing status', () => { + store.commit(types.SET_PROJECT_BILLING_STATUS, true); + + expect(store.state.projectHasBillingEnabled).toBeTruthy(); + }); + }); + + describe('SET_ZONE', () => { + it('should set GCP zone as selectedZone', () => { + const zoneToSelect = gapiZonesResponseMock.items[0].name; + + store.commit(types.SET_ZONE, zoneToSelect); + + expect(store.state.selectedZone).toEqual(selectedZoneMock); + }); + }); + + describe('SET_MACHINE_TYPE', () => { + it('should set GCP machine type as selectedMachineType', () => { + const machineTypeToSelect = gapiMachineTypesResponseMock.items[0].name; + + store.commit(types.SET_MACHINE_TYPE, machineTypeToSelect); + + expect(store.state.selectedMachineType).toEqual(selectedMachineTypeMock); + }); + }); + + describe('SET_PROJECTS', () => { + it('should set Google API Projects response as projects', () => { + expect(store.state.projects.length).toEqual(0); + + store.commit(types.SET_PROJECTS, gapiProjectsResponseMock.projects); + + expect(store.state.projects.length).toEqual(gapiProjectsResponseMock.projects.length); + }); + }); + + describe('SET_ZONES', () => { + it('should set Google API Zones response as zones', () => { + expect(store.state.zones.length).toEqual(0); + + store.commit(types.SET_ZONES, gapiZonesResponseMock.items); + + expect(store.state.zones.length).toEqual(gapiZonesResponseMock.items.length); + }); + }); + + describe('SET_MACHINE_TYPES', () => { + it('should set Google API Machine Types response as machineTypes', () => { + expect(store.state.machineTypes.length).toEqual(0); + + store.commit(types.SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items); + + expect(store.state.machineTypes.length).toEqual(gapiMachineTypesResponseMock.items.length); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js new file mode 100644 index 00000000000..ba897f4660d --- /dev/null +++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js @@ -0,0 +1,69 @@ +import Vue from 'vue'; + +import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue'; + +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +const defaultLabel = 'Select'; +const customLabel = 'Select project'; + +const createComponent = config => { + const Component = Vue.extend(dropdownButtonComponent); + + return mountComponent(Component, config); +}; + +describe('DropdownButtonComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('dropdownToggleText', () => { + it('returns default toggle text', () => { + expect(vm.toggleText).toBe(defaultLabel); + }); + + it('returns custom toggle text when provided via props', () => { + const vmEmptyLabels = createComponent({ toggleText: customLabel }); + + expect(vmEmptyLabels.toggleText).toBe(customLabel); + vmEmptyLabels.$destroy(); + }); + }); + }); + + describe('template', () => { + it('renders component container element of type `button`', () => { + expect(vm.$el.nodeName).toBe('BUTTON'); + }); + + it('renders component container element with required data attributes', () => { + expect(vm.$el.dataset.abilityName).toBe(vm.abilityName); + expect(vm.$el.dataset.fieldName).toBe(vm.fieldName); + expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath); + expect(vm.$el.dataset.labels).toBe(vm.labelsPath); + expect(vm.$el.dataset.namespacePath).toBe(vm.namespace); + expect(vm.$el.dataset.showAny).not.toBeDefined(); + }); + + it('renders dropdown toggle text element', () => { + const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); + expect(dropdownToggleTextEl).not.toBeNull(); + expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel); + }); + + it('renders dropdown button icon', () => { + const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa'); + + expect(dropdownIconEl).not.toBeNull(); + expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js index 88733922a59..445ab0cb40e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js +++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js @@ -1,17 +1,17 @@ import Vue from 'vue'; -import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue'; +import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockLabels } from './mock_data'; -const createComponent = (name = 'label_id[]', label = mockLabels[0]) => { +const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => { const Component = Vue.extend(dropdownHiddenInputComponent); return mountComponent(Component, { name, - label, + value, }); }; @@ -31,7 +31,7 @@ describe('DropdownHiddenInputComponent', () => { expect(vm.$el.nodeName).toBe('INPUT'); expect(vm.$el.getAttribute('type')).toBe('hidden'); expect(vm.$el.getAttribute('name')).toBe(vm.name); - expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`); + expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`); }); }); }); diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js new file mode 100644 index 00000000000..551520721e5 --- /dev/null +++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js @@ -0,0 +1,52 @@ +import Vue from 'vue'; + +import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; + +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +const componentConfig = { + placeholderText: 'Search something', +}; + +const createComponent = (config = componentConfig) => { + const Component = Vue.extend(dropdownSearchInputComponent); + + return mountComponent(Component, config); +}; + +describe('DropdownSearchInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element with type `search`', () => { + const inputEl = vm.$el.querySelector('input.dropdown-input-field'); + + expect(inputEl).not.toBeNull(); + expect(inputEl.getAttribute('type')).toBe('search'); + }); + + it('renders search icon element', () => { + expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull(); + }); + + it('renders clear search icon element', () => { + expect( + vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'), + ).not.toBeNull(); + }); + + it('displays custom placeholder text', () => { + const inputEl = vm.$el.querySelector('input.dropdown-input-field'); + + expect(inputEl.getAttribute('placeholder')).toBe(componentConfig.placeholderText); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/javascripts/vue_shared/components/dropdown/mock_data.js new file mode 100644 index 00000000000..b09d42da401 --- /dev/null +++ b/spec/javascripts/vue_shared/components/dropdown/mock_data.js @@ -0,0 +1,11 @@ +export const mockLabels = [ + { + id: 26, + title: 'Foo Label', + description: 'Foobar', + color: '#BADA55', + text_color: '#FFFFFF', + }, +]; + +export default mockLabels; diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index a44243ac82d..023bedaaebb 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -71,6 +71,40 @@ describe Backup::Repository do end end + describe '#delete_all_repositories', :seed_helper do + shared_examples('delete_all_repositories') do + before do + allow(FileUtils).to receive(:mkdir_p).and_call_original + allow(FileUtils).to receive(:mv).and_call_original + end + + after(:all) do + ensure_seeds + end + + it 'removes all repositories' do + # Sanity check: there should be something for us to delete + expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) + + subject.delete_all_repositories('default', Gitlab.config.repositories.storages['default']) + + expect(list_repositories).to be_empty + end + + def list_repositories + Dir[SEED_STORAGE_PATH + '/*.git'] + end + end + + context 'with gitaly' do + it_behaves_like 'delete_all_repositories' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like 'delete_all_repositories' + end + end + describe '#empty_repo?' do context 'for a wiki' do let(:wiki) { create(:project_wiki) } diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb index fa209bed74e..002ce776be9 100644 --- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb +++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb @@ -22,7 +22,8 @@ describe Gitlab::Auth::UserAccessDeniedReason do enforce_terms end - it { is_expected.to match /You must accept the Terms of Service/ } + it { is_expected.to match /must accept the Terms of Service/ } + it { is_expected.to include(user.username) } end context 'when the user is internal' do diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb index 9d9caaabe16..407d9470785 100644 --- a/spec/lib/gitlab/database/count_spec.rb +++ b/spec/lib/gitlab/database/count_spec.rb @@ -3,59 +3,68 @@ require 'spec_helper' describe Gitlab::Database::Count do before do create_list(:project, 3) + create(:identity) end - describe '.execute_estimate_if_updated_recently', :postgresql do - context 'when reltuples have not been updated' do - before do - expect(described_class).to receive(:reltuples_updated_recently?).and_return(false) - end + let(:models) { [Project, Identity] } - it 'returns nil' do - expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil - end - end + describe '.approximate_counts' do + context 'with MySQL' do + context 'when reltuples have not been updated' do + it 'counts all models the normal way' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) - context 'when reltuples have been updated' do - before do - ActiveRecord::Base.connection.execute('ANALYZE projects') - end + expect(Project).to receive(:count).and_call_original + expect(Identity).to receive(:count).and_call_original - it 'calls postgresql_estimate_query' do - expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original - expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3) + expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 }) + end end end - end - describe '.approximate_count' do - context 'when reltuples have not been updated' do - it 'counts all projects the normal way' do - allow(described_class).to receive(:reltuples_updated_recently?).and_return(false) + context 'with PostgreSQL', :postgresql do + describe 'when reltuples have not been updated' do + it 'counts all models the normal way' do + expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({}) - expect(Project).to receive(:count).and_call_original - expect(described_class.approximate_count(Project)).to eq(3) + expect(Project).to receive(:count).and_call_original + expect(Identity).to receive(:count).and_call_original + expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 }) + end end - end - context 'no permission' do - it 'falls back to standard query' do - allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege) + describe 'no permission' do + it 'falls back to standard query' do + allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege) - expect(Project).to receive(:count).and_call_original - expect(described_class.approximate_count(Project)).to eq(3) + expect(Project).to receive(:count).and_call_original + expect(Identity).to receive(:count).and_call_original + expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 }) + end end - end - describe 'when reltuples have been updated', :postgresql do - before do - ActiveRecord::Base.connection.execute('ANALYZE projects') + describe 'when some reltuples have been updated' do + it 'counts projects in the fast way' do + expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 }) + + expect(Project).not_to receive(:count).and_call_original + expect(Identity).to receive(:count).and_call_original + expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 }) + end end - it 'counts all projects in the fast way' do - expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original + describe 'when all reltuples have been updated' do + before do + ActiveRecord::Base.connection.execute('ANALYZE projects') + ActiveRecord::Base.connection.execute('ANALYZE identities') + end + + it 'counts models with the standard way' do + expect(Project).not_to receive(:count) + expect(Identity).not_to receive(:count) - expect(described_class.approximate_count(Project)).to eq(3) + expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 }) + end end end end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 0c2e18c268a..0588fe935c3 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -468,4 +468,58 @@ describe Gitlab::Diff::File do end end end + + describe '#diff_hunk' do + let(:raw_diff) do + <<EOS +@@ -6,12 +6,18 @@ module Popen + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) +- raise "System commands must be given as an array of strings" ++ raise RuntimeError, "System commands must be given as an array of strings" + end + + path ||= Dir.pwd +- vars = { "PWD" => path } +- options = { chdir: path } ++ ++ vars = { ++ "PWD" => path ++ } ++ ++ options = { ++ chdir: path ++ } + + unless File.directory?(path) + FileUtils.mkdir_p(path) +@@ -19,6 +25,7 @@ module Popen + + @cmd_output = "" + @cmd_status = 0 ++ + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read +EOS + end + + it 'returns raw diff up to given line index' do + allow(diff_file).to receive(:raw_diff) { raw_diff } + diff_line = instance_double(Gitlab::Diff::Line, index: 5) + + diff_hunk = <<EOS +@@ -6,12 +6,18 @@ module Popen + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) +- raise "System commands must be given as an array of strings" ++ raise RuntimeError, "System commands must be given as an array of strings" + end +EOS + + expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk) + end + end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 317a932d5a6..dfffea7797f 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1055,7 +1055,7 @@ describe Gitlab::GitAccess do it 'blocks access when the user did not accept terms', :aggregate_failures do actions.each do |action| - expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/) + expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/) end end diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb new file mode 100644 index 00000000000..f47b9dd3498 --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do + subject { described_class.new } + + describe ".parameters" do + let(:start_time) { Time.new(2018, 01, 01) } + + describe 'when no proxy time is available' do + let(:mock_request) { OpenStruct.new(env: {}) } + + it 'returns an empty hash' do + expect(subject.parameters(mock_request, nil)).to eq({}) + end + end + + describe 'when a proxy time is available' do + let(:mock_request) do + OpenStruct.new( + env: { + 'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9) + } + ) + end + + it 'returns the correct duration in ms' do + Timecop.freeze(start_time) do + subject.before + + expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration': 1.hour.to_f * 1000 }) + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8b46b04b8b5..fb5fd300dbb 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -35,6 +35,7 @@ notes: - todos - events - system_note_metadata +- note_diff_file label_links: - target - label diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index cd5a1b2982b..536cc359d39 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -15,7 +15,10 @@ describe Gitlab::ImportExport::AttributeCleaner do 'project_id' => 99, 'user_id' => 99, 'random_id_in_the_middle' => 99, - 'notid' => 99 + 'notid' => 99, + 'import_source' => 'whatever', + 'import_type' => 'whatever', + 'non_existent_attr' => 'whatever' } end @@ -28,10 +31,30 @@ describe Gitlab::ImportExport::AttributeCleaner do } end + let(:excluded_keys) { %w[import_source import_type] } + + subject { described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class, excluded_keys: excluded_keys) } + + before do + allow(relation_class).to receive(:attribute_method?).and_return(true) + allow(relation_class).to receive(:attribute_method?).with('non_existent_attr').and_return(false) + end + it 'removes unwanted attributes from the hash' do - # allow(relation_class).to receive(:attribute_method?).and_return(true) + expect(subject).to eq(post_safe_hash) + end + + it 'removes attributes not present in relation_class' do + expect(subject.keys).not_to include 'non_existent_attr' + end + + it 'removes excluded keys from the hash' do + expect(subject.keys).not_to include excluded_keys + end + + it 'does not remove excluded key if not listed' do parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class) - expect(parsed_hash).to eq(post_safe_hash) + expect(parsed_hash.keys).to eq post_safe_hash.keys + excluded_keys end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4f64f2bd6b4..1b7fa11cb3c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -1,5 +1,7 @@ { "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "import_type": "gitlab_project", + "creator_id": 123, "visibility_level": 10, "archived": false, "labels": [ diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index 5dbf0ed289b..c13cf4a0507 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -1,5 +1,7 @@ { "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "import_type": "gitlab_project", + "creator_id": 123, "visibility_level": 10, "archived": false, "milestones": [ 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 13a8c9adcee..68ddc947e02 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -23,6 +23,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch) project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project) + + expect(Gitlab::ImportExport::RelationFactory).to receive(:create).with(hash_including(excluded_keys: ['whatever'])).and_call_original.at_least(:once) + allow(project_tree_restorer).to receive(:excluded_keys_for_relation).and_return(['whatever']) + @restored_project_json = project_tree_restorer.restore end end @@ -248,6 +252,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do 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 + + it 'does not set params that are excluded from import_export settings' do + expect(project.import_type).to be_nil + expect(project.creator_id).not_to eq 123 + end end shared_examples 'restores group correctly' do |**results| diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index 5c61a5a2044..5f0dfd64b15 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -4,12 +4,14 @@ describe Gitlab::ImportExport::RelationFactory do let(:project) { create(:project) } let(:members_mapper) { double('members_mapper').as_null_object } let(:user) { create(:admin) } + let(:excluded_keys) { [] } let(:created_object) do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, members_mapper: members_mapper, user: user, - project: project) + project: project, + excluded_keys: excluded_keys) end context 'hook object' do @@ -67,6 +69,14 @@ describe Gitlab::ImportExport::RelationFactory do expect(created_object.service_id).not_to eq(service_id) end end + + context 'excluded attributes' do + let(:excluded_keys) { %w[url] } + + it 'are removed from the imported object' do + expect(created_object.url).to be_nil + end + end end # Mocks an ActiveRecordish object with the dodgy columns diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index db9d9158b29..27cb3198e5b 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -50,30 +50,6 @@ 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/migrations/fill_file_store_spec.rb b/spec/migrations/fill_file_store_spec.rb new file mode 100644 index 00000000000..5ff7aa56ce2 --- /dev/null +++ b/spec/migrations/fill_file_store_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180424151928_fill_file_store') + +describe FillFileStore, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + let(:lfs_objects) { table(:lfs_objects) } + let(:uploads) { table(:uploads) } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + builds.create!(id: 1) + + ## + # Create rows that have nullfied `file_store` column + job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: nil) + lfs_objects.create!(oid: 123, size: 10, file: 'file_name', file_store: nil) + uploads.create!(size: 10, path: 'path', uploader: 'uploader', mount_point: 'file_name', store: nil) + end + + it 'correctly migrates nullified file_store/store column' do + expect(job_artifacts.where(file_store: nil).count).to eq(1) + expect(lfs_objects.where(file_store: nil).count).to eq(1) + expect(uploads.where(store: nil).count).to eq(1) + + expect(job_artifacts.where(file_store: 1).count).to eq(0) + expect(lfs_objects.where(file_store: 1).count).to eq(0) + expect(uploads.where(store: 1).count).to eq(0) + + migrate! + + expect(job_artifacts.where(file_store: nil).count).to eq(0) + expect(lfs_objects.where(file_store: nil).count).to eq(0) + expect(uploads.where(store: nil).count).to eq(0) + + expect(job_artifacts.where(file_store: 1).count).to eq(1) + expect(lfs_objects.where(file_store: 1).count).to eq(1) + expect(uploads.where(store: 1).count).to eq(1) + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 7e47043a1cb..f8f07205623 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -391,68 +391,6 @@ describe ApplicationSetting do end describe 'performance bar settings' do - describe 'performance_bar_allowed_group_id=' do - context 'with a blank path' do - before do - setting.performance_bar_allowed_group_id = create(:group).full_path - end - - it 'persists nil for a "" path and clears allowed user IDs cache' do - expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_allowed_group_id = '' - - expect(setting.performance_bar_allowed_group_id).to be_nil - end - end - - context 'with an invalid path' do - it 'does not persist an invalid group path' do - setting.performance_bar_allowed_group_id = 'foo' - - expect(setting.performance_bar_allowed_group_id).to be_nil - end - end - - context 'with a path to an existing group' do - let(:group) { create(:group) } - - it 'persists a valid group path and clears allowed user IDs cache' do - expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_allowed_group_id = group.full_path - - expect(setting.performance_bar_allowed_group_id).to eq(group.id) - end - - context 'when the given path is the same' do - context 'with a blank path' do - before do - setting.performance_bar_allowed_group_id = nil - end - - it 'clears the cached allowed user IDs' do - expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_allowed_group_id = '' - end - end - - context 'with a valid path' do - before do - setting.performance_bar_allowed_group_id = group.full_path - end - - it 'clears the cached allowed user IDs' do - expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_allowed_group_id = group.full_path - end - end - end - end - end - describe 'performance_bar_allowed_group' do context 'with no performance_bar_allowed_group_id saved' do it 'returns nil' do @@ -464,11 +402,11 @@ describe ApplicationSetting do let(:group) { create(:group) } before do - setting.performance_bar_allowed_group_id = group.full_path + setting.update!(performance_bar_allowed_group_id: group.id) end it 'returns the group' do - expect(setting.performance_bar_allowed_group).to eq(group) + expect(setting.reload.performance_bar_allowed_group).to eq(group) end end end @@ -478,67 +416,11 @@ describe ApplicationSetting do let(:group) { create(:group) } before do - setting.performance_bar_allowed_group_id = group.full_path + setting.update!(performance_bar_allowed_group_id: group.id) end it 'returns true' do - expect(setting.performance_bar_enabled).to be_truthy - end - end - end - - describe 'performance_bar_enabled=' do - context 'when the performance bar is enabled' do - let(:group) { create(:group) } - - before do - setting.performance_bar_allowed_group_id = group.full_path - end - - context 'when passing true' do - it 'does not clear allowed user IDs cache' do - expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_enabled = true - - expect(setting.performance_bar_allowed_group_id).to eq(group.id) - expect(setting.performance_bar_enabled).to be_truthy - end - end - - context 'when passing false' do - it 'disables the performance bar and clears allowed user IDs cache' do - expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_enabled = false - - expect(setting.performance_bar_allowed_group_id).to be_nil - expect(setting.performance_bar_enabled).to be_falsey - end - end - end - - context 'when the performance bar is disabled' do - context 'when passing true' do - it 'does nothing and does not clear allowed user IDs cache' do - expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_enabled = true - - expect(setting.performance_bar_allowed_group_id).to be_nil - expect(setting.performance_bar_enabled).to be_falsey - end - end - - context 'when passing false' do - it 'does nothing and does not clear allowed user IDs cache' do - expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) - - setting.performance_bar_enabled = false - - expect(setting.performance_bar_allowed_group_id).to be_nil - expect(setting.performance_bar_enabled).to be_falsey - end + expect(setting.reload.performance_bar_enabled).to be_truthy end end end diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb new file mode 100644 index 00000000000..c16b245bea8 --- /dev/null +++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe BatchDestroyDependentAssociations do + class TestProject < ActiveRecord::Base + self.table_name = 'projects' + + has_many :builds, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :pages_domains + has_many :todos + + include BatchDestroyDependentAssociations + end + + describe '#dependent_associations_to_destroy' do + set(:project) { TestProject.new } + + it 'returns the right associations' do + expect(project.dependent_associations_to_destroy.map(&:name)).to match_array([:builds]) + end + end + + describe '#destroy_dependent_associations_in_batches' do + set(:project) { create(:project) } + set(:build) { create(:ci_build, project: project) } + set(:notification_setting) { create(:notification_setting, project: project) } + let!(:todos) { create(:todo, project: project) } + + it 'destroys multiple builds' do + create(:ci_build, project: project) + + expect(Ci::Build.count).to eq(2) + + project.destroy_dependent_associations_in_batches + + expect(Ci::Build.count).to eq(0) + end + + it 'destroys builds in batches' do + expect(project).to receive_message_chain(:builds, :find_each).and_yield(build) + expect(build).to receive(:destroy).and_call_original + + project.destroy_dependent_associations_in_batches + + expect(Ci::Build.count).to eq(0) + expect(Todo.count).to eq(1) + expect(User.count).to be > 0 + expect(NotificationSetting.count).to eq(User.count) + end + + it 'excludes associations' do + project.destroy_dependent_associations_in_batches(exclude: [:builds]) + + expect(Ci::Build.count).to eq(1) + expect(Todo.count).to eq(1) + expect(User.count).to be > 0 + expect(NotificationSetting.count).to eq(User.count) + end + end +end diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb index 30572ce9332..8cd129dc851 100644 --- a/spec/models/concerns/discussion_on_diff_spec.rb +++ b/spec/models/concerns/discussion_on_diff_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe DiscussionOnDiff do - subject { create(:diff_note_on_merge_request).to_discussion } + subject { create(:diff_note_on_merge_request, line_number: 18).to_discussion } describe "#truncated_diff_lines" do let(:truncated_lines) { subject.truncated_diff_lines } diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index fb51c0172ab..8624f0daa4d 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -84,7 +84,47 @@ describe DiffNote do end end - describe "#diff_file" do + describe '#create_diff_file callback' do + let(:noteable) { create(:merge_request) } + let(:project) { noteable.project } + + context 'merge request' do + let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) } + + it 'creates a diff note file' do + expect(diff_note.reload.note_diff_file).to be_present + end + + it 'does not create diff note file if it is a reply' do + expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) } + .not_to change(NoteDiffFile, :count) + end + end + + context 'commit' do + let!(:diff_note) { create(:diff_note_on_commit, project: project) } + + it 'creates a diff note file' do + expect(diff_note.reload.note_diff_file).to be_present + end + + it 'does not create diff note file if it is a reply' do + expect { create(:diff_note_on_commit, in_reply_to: diff_note) } + .not_to change(NoteDiffFile, :count) + end + end + end + + describe '#diff_file', :clean_gitlab_redis_shared_state do + context 'when note_diff_file association exists' do + it 'returns persisted diff file data' do + diff_file = subject.diff_file + + expect(diff_file.diff.to_hash.with_indifferent_access) + .to include(subject.note_diff_file.to_hash) + end + end + context 'when the discussion was created in the diff' do it 'returns correct diff file' do diff_file = subject.diff_file @@ -115,6 +155,26 @@ describe DiffNote do expect(diff_file.diff_refs).to eq(position.diff_refs) end end + + context 'note diff file creation enqueuing' do + it 'enqueues CreateNoteDiffFileWorker if it is the first note of a discussion' do + subject.note_diff_file.destroy! + + expect(CreateNoteDiffFileWorker).to receive(:perform_async).with(subject.id) + + subject.reload.diff_file + end + + it 'does not enqueues CreateNoteDiffFileWorker if not first note of a discussion' do + mr = create(:merge_request) + diff_note = create(:diff_note_on_merge_request, project: mr.project, noteable: mr) + reply_diff_note = create(:diff_note_on_merge_request, in_reply_to: diff_note) + + expect(CreateNoteDiffFileWorker).not_to receive(:perform_async).with(reply_diff_note.id) + + reply_diff_note.reload.diff_file + end + end end describe "#diff_line" do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 8ea92410022..c1eac4fa489 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -50,6 +50,19 @@ describe Event do end end + describe '#set_last_repository_updated_at' do + it 'only updates once every Event::REPOSITORY_UPDATED_AT_INTERVAL minutes' do + last_known_timestamp = (Event::REPOSITORY_UPDATED_AT_INTERVAL - 1.minute).ago + project.update(last_repository_updated_at: last_known_timestamp) + project.reload # a reload removes fractions of seconds + + expect do + create_push_event(project, project.owner) + project.reload + end.not_to change { project.last_repository_updated_at } + end + end + describe 'after_create :track_user_interacted_projects' do let(:event) { build(:push_event, project: project, author: project.owner) } diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 8ef91e8fab5..581fd0293cc 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -5,7 +5,7 @@ describe InternalId do let(:usage) { :issues } let(:issue) { build(:issue, project: project) } let(:scope) { { project: project } } - let(:init) { ->(s) { s.project.issues.maximum(:iid) } } + let(:init) { ->(s) { s.project.issues.size } } context 'validations' do it { is_expected.to validate_presence_of(:usage) } @@ -39,29 +39,6 @@ describe InternalId do end end - context 'with an InternalId record present and existing issues with a higher internal id' do - # This can happen if the old NonatomicInternalId is still in use - before do - issues = Array.new(rand(1..10)).map { create(:issue, project: project) } - - issue = issues.last - issue.iid = issues.map { |i| i.iid }.max + 1 - issue.save - end - - let(:maximum_iid) { project.issues.map { |i| i.iid }.max } - - it 'updates last_value to the maximum internal id present' do - subject - - expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1) - end - - it 'returns next internal id correctly' do - expect(subject).to eq(maximum_iid + 1) - end - end - context 'with concurrent inserts on table' do it 'looks up the record if it was created concurrently' do args = { **scope, usage: described_class.usages[usage.to_s] } @@ -104,8 +81,7 @@ describe InternalId do describe '#increment_and_save!' do let(:id) { create(:internal_id) } - let(:maximum_iid) { nil } - subject { id.increment_and_save!(maximum_iid) } + subject { id.increment_and_save! } it 'returns incremented iid' do value = id.last_value @@ -126,14 +102,5 @@ describe InternalId do expect(subject).to eq(1) end end - - context 'with maximum_iid given' do - let(:id) { create(:internal_id, last_value: 1) } - let(:maximum_iid) { id.last_value + 10 } - - it 'returns maximum_iid instead' do - expect(subject).to eq(12) - end - end end end diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb new file mode 100644 index 00000000000..591c1a89748 --- /dev/null +++ b/spec/models/note_diff_file_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +describe NoteDiffFile do + describe 'associations' do + it { is_expected.to belong_to(:diff_note) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:diff_note) } + end +end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index ae9c0e9c304..32fc704a79b 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -171,7 +171,7 @@ describe API::DeployKeys do deploy_key end - it 'deletes existing key' do + it 'removes existing key from project' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) @@ -179,6 +179,44 @@ describe API::DeployKeys do end.to change { project.deploy_keys.count }.by(-1) end + context 'when the deploy key is public' do + it 'does not delete the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.not_to change { DeployKey.count } + end + end + + context 'when the deploy key is not public' do + let!(:deploy_key) { create(:deploy_key, public: false) } + + context 'when the deploy key is only used by this project' do + it 'deletes the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.to change { DeployKey.count }.by(-1) + end + end + + context 'when the deploy key is used by other projects' do + before do + create(:deploy_keys_project, project: project2, deploy_key: deploy_key) + end + + it 'does not delete the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.not_to change { DeployKey.count } + end + end + end + it 'returns 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/deploy_keys/404", admin) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index d3ab44c0d7e..d8a51f36dba 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -171,7 +171,7 @@ describe API::Helpers do end it 'returns a 403 when a user has not accepted the terms' do - expect { current_user }.to raise_error /You must accept the Terms of Service/ + expect { current_user }.to raise_error /must accept the Terms of Service/ end it 'sets the current user when the user accepted the terms' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index db8c5f963d6..5dc3ddd4b36 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe API::Internal do - let(:user) { create(:user) } + set(:user) { create(:user) } let(:key) { create(:key, user: user) } - let(:project) { create(:project, :repository, :wiki_repo) } + set(:project) { create(:project, :repository, :wiki_repo) } let(:secret_token) { Gitlab::Shell.secret_token } let(:gl_repository) { "project-#{project.id}" } let(:reference_counter) { double('ReferenceCounter') } @@ -277,7 +277,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) + expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user).not_to have_an_activity_record end @@ -289,7 +289,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) + expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user).to have_an_activity_record end @@ -301,7 +301,7 @@ describe API::Internal do 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["repository_path"]).to eq('/') 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 @@ -320,7 +320,7 @@ describe API::Internal do 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["repository_path"]).to eq('/') 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 diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 0a2963452e4..45082e644ca 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -13,7 +13,10 @@ describe API::Jobs do ref: project.default_branch) end - let!(:job) { create(:ci_build, :success, pipeline: pipeline) } + let!(:job) do + create(:ci_build, :success, pipeline: pipeline, + artifacts_expire_at: 1.day.since) + end let(:user) { create(:user) } let(:api_user) { user } @@ -43,6 +46,7 @@ describe API::Jobs do it 'returns correct values' do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id + expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) end it 'returns pipeline data' do @@ -128,6 +132,7 @@ describe API::Jobs do it 'returns correct values' do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id + expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) end it 'returns pipeline data' do @@ -201,6 +206,7 @@ describe API::Jobs do expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at) expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at) expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at) + expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) expect(json_response['duration']).to eq(job.duration) end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index efb9bddde44..6aadf839dbd 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -830,6 +830,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when job has already been finished' do + before do + job.trace.set('Job failed') + job.drop!(:script_failure) + end + + it 'does not update job status and job trace' do + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + + expect(response).to have_gitlab_http_status(403) + expect(job.trace.raw).to eq 'Job failed' + expect(job).to be_failed + end + end + def update_job(token = job.token, **params) new_params = params.merge(token: token) put api("/jobs/#{job.id}"), new_params @@ -1210,7 +1225,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do before do fog_connection.directories.get('artifacts').files.create( - key: 'tmp/upload/12312300', + key: 'tmp/uploads/12312300', body: 'content' ) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 8b22d1e72f3..aead8978dd4 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -24,10 +24,15 @@ describe API::Settings, 'Settings' do expect(json_response['ecdsa_key_restriction']).to eq(0) expect(json_response['ed25519_key_restriction']).to eq(0) expect(json_response['circuitbreaker_failure_count_threshold']).not_to be_nil + expect(json_response['performance_bar_allowed_group_id']).to be_nil + expect(json_response).not_to have_key('performance_bar_allowed_group_path') + expect(json_response).not_to have_key('performance_bar_enabled') end end describe "PUT /application/settings" do + let(:group) { create(:group) } + context "custom repository storage type set in the config" do before do storages = { 'custom' => 'tmp/tests/custom_repositories' } @@ -56,7 +61,8 @@ describe API::Settings, 'Settings' do ed25519_key_restriction: 256, circuitbreaker_check_interval: 2, enforce_terms: true, - terms: 'Hello world!' + terms: 'Hello world!', + performance_bar_allowed_group_path: group.full_path expect(response).to have_gitlab_http_status(200) expect(json_response['default_projects_limit']).to eq(3) @@ -80,9 +86,27 @@ describe API::Settings, 'Settings' do expect(json_response['circuitbreaker_check_interval']).to eq(2) expect(json_response['enforce_terms']).to be(true) expect(json_response['terms']).to eq('Hello world!') + expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) end end + it "supports legacy performance_bar_allowed_group_id" do + put api("/application/settings", admin), + performance_bar_allowed_group_id: group.full_path + + expect(response).to have_gitlab_http_status(200) + expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) + end + + it "supports legacy performance_bar_enabled" do + put api("/application/settings", admin), + performance_bar_enabled: false, + performance_bar_allowed_group_id: group.full_path + + expect(response).to have_gitlab_http_status(200) + expect(json_response['performance_bar_allowed_group_id']).to be_nil + end + context "missing koding_url value when koding_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), koding_enabled: true diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index f80abb06fca..79672fe1cc5 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1089,7 +1089,7 @@ describe 'Git LFS API and storage' do context 'with valid remote_id' do before do fog_connection.directories.get('lfs-objects').files.create( - key: 'tmp/upload/12312300', + key: 'tmp/uploads/12312300', body: 'content' ) 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 index 7ddf9141fcd..03eeffe6483 100644 --- a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb +++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb @@ -256,6 +256,18 @@ describe RuboCop::Cop::LineBreakAroundConditionalBlock do expect(cop.offenses).to be_empty end + it "doesn't flag violation for #{conditional} followed by a comment" do + source = <<~RUBY + #{conditional} condition + do_something + end + # a short comment + 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 diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index fb07ecc6ae8..6337ee7d724 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ApplicationSettings::UpdateService do - let(:application_settings) { Gitlab::CurrentSettings.current_application_settings } + let(:application_settings) { create(:application_setting) } let(:admin) { create(:user, :admin) } let(:params) { {} } @@ -54,4 +54,90 @@ describe ApplicationSettings::UpdateService do end end end + + describe 'performance bar settings' do + using RSpec::Parameterized::TableSyntax + + where(:params_performance_bar_enabled, + :params_performance_bar_allowed_group_path, + :previous_performance_bar_allowed_group_id, + :expected_performance_bar_allowed_group_id) do + true | '' | nil | nil + true | '' | 42_000_000 | nil + true | nil | nil | nil + true | nil | 42_000_000 | nil + true | 'foo' | nil | nil + true | 'foo' | 42_000_000 | nil + true | 'group_a' | nil | 42_000_000 + true | 'group_b' | 42_000_000 | 43_000_000 + true | 'group_a' | 42_000_000 | 42_000_000 + false | '' | nil | nil + false | '' | 42_000_000 | nil + false | nil | nil | nil + false | nil | 42_000_000 | nil + false | 'foo' | nil | nil + false | 'foo' | 42_000_000 | nil + false | 'group_a' | nil | nil + false | 'group_b' | 42_000_000 | nil + false | 'group_a' | 42_000_000 | nil + end + + with_them do + let(:params) do + { + performance_bar_enabled: params_performance_bar_enabled, + performance_bar_allowed_group_path: params_performance_bar_allowed_group_path + } + end + + before do + if previous_performance_bar_allowed_group_id == 42_000_000 || params_performance_bar_allowed_group_path == 'group_a' + create(:group, id: 42_000_000, path: 'group_a') + end + + if expected_performance_bar_allowed_group_id == 43_000_000 || params_performance_bar_allowed_group_path == 'group_b' + create(:group, id: 43_000_000, path: 'group_b') + end + + application_settings.update!(performance_bar_allowed_group_id: previous_performance_bar_allowed_group_id) + end + + it 'sets performance_bar_allowed_group_id when present and performance_bar_enabled == true' do + expect(application_settings.performance_bar_allowed_group_id).to eq(previous_performance_bar_allowed_group_id) + + if previous_performance_bar_allowed_group_id != expected_performance_bar_allowed_group_id + expect { subject.execute } + .to change(application_settings, :performance_bar_allowed_group_id) + .from(previous_performance_bar_allowed_group_id).to(expected_performance_bar_allowed_group_id) + else + expect { subject.execute } + .not_to change(application_settings, :performance_bar_allowed_group_id) + end + end + end + + context 'when :performance_bar_allowed_group_path is not present' do + let(:group) { create(:group) } + + before do + application_settings.update!(performance_bar_allowed_group_id: group.id) + end + + it 'does not change the performance bar settings' do + expect { subject.execute } + .not_to change(application_settings, :performance_bar_allowed_group_id) + end + end + + context 'when :performance_bar_enabled is not present' do + let(:group) { create(:group) } + let(:params) { { performance_bar_allowed_group_path: group.full_path } } + + it 'implicitely defaults to true' do + expect { subject.execute } + .to change(application_settings, :performance_bar_allowed_group_id) + .from(nil).to(group.id) + end + end + end end diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb deleted file mode 100644 index 3e68d906e71..00000000000 --- a/spec/services/check_gcp_project_billing_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -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/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index f5cff66de6d..2b2b983494f 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -57,6 +57,88 @@ describe Notes::CreateService do end end + context 'note diff file' do + let(:project_with_repo) { create(:project, :repository) } + let(:merge_request) do + create(:merge_request, + source_project: project_with_repo, + target_project: project_with_repo) + end + let(:line_number) { 14 } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: line_number, + diff_refs: merge_request.diff_refs) + end + let(:previous_note) do + create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo) + end + + context 'when eligible to have a note diff file' do + let(:new_opts) do + opts.merge(in_reply_to_discussion_id: nil, + type: 'DiffNote', + noteable_type: 'MergeRequest', + noteable_id: merge_request.id, + position: position.to_h) + end + + it 'note is associated with a note diff file' do + note = described_class.new(project_with_repo, user, new_opts).execute + + expect(note).to be_persisted + expect(note.note_diff_file).to be_present + end + end + + context 'when DiffNote is a reply' do + let(:new_opts) do + opts.merge(in_reply_to_discussion_id: previous_note.discussion_id, + type: 'DiffNote', + noteable_type: 'MergeRequest', + noteable_id: merge_request.id, + position: position.to_h) + end + + it 'note is not associated with a note diff file' do + note = described_class.new(project_with_repo, user, new_opts).execute + + expect(note).to be_persisted + expect(note.note_diff_file).to be_nil + end + + context 'when DiffNote from an image' do + let(:image_position) do + Gitlab::Diff::Position.new(old_path: "files/images/6049019_460s.jpg", + new_path: "files/images/6049019_460s.jpg", + width: 100, + height: 100, + x: 1, + y: 100, + diff_refs: merge_request.diff_refs, + position_type: 'image') + end + + let(:new_opts) do + opts.merge(in_reply_to_discussion_id: nil, + type: 'DiffNote', + noteable_type: 'MergeRequest', + noteable_id: merge_request.id, + position: image_position.to_h) + end + + it 'note is not associated with a note diff file' do + note = described_class.new(project_with_repo, user, new_opts).execute + + expect(note).to be_persisted + expect(note.note_diff_file).to be_nil + end + end + end + end + context 'note with commands' do context 'as a user who can update the target' do context '/close, /label, /assign & /milestone' do diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index e7277b337f6..4165a005063 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -618,7 +618,7 @@ describe ObjectStorage do let!(:fog_file) do fog_connection.directories.get('uploads').files.create( - key: 'tmp/upload/test/123123', + key: 'tmp/uploads/test/123123', body: 'content' ) end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb index 59c777ea338..0e8b7c82d3a 100644 --- a/spec/views/admin/dashboard/index.html.haml_spec.rb +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -4,6 +4,11 @@ describe 'admin/dashboard/index.html.haml' do include Devise::Test::ControllerHelpers before do + counts = Admin::DashboardController::COUNTED_ITEMS.each_with_object({}) do |item, hash| + hash[item] = 100 + end + + assign(:counts, counts) assign(:projects, create_list(:project, 1)) assign(:users, create_list(:user, 1)) assign(:groups, create_list(:group, 1)) diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb deleted file mode 100644 index 526ecf75921..00000000000 --- a/spec/workers/check_gcp_project_billing_worker_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -require 'spec_helper' - -describe CheckGcpProjectBillingWorker do - describe '.perform' do - let(:token) { 'bogustoken' } - - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:get_billing_state) - allow_any_instance_of(described_class).to receive(:update_billing_change_counter) - end - - 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 - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) - expect(described_class).to receive(:set_billing_state).with(token, true) - - 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(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 - - describe 'billing change counter' do - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:get_session_token).and_return('bogustoken') - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - allow(described_class).to receive(:set_billing_state) - end - - context 'when previous state was false' do - before do - expect(described_class).to receive(:get_billing_state).and_return(false) - end - - context 'when the current state is false' do - before do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - - context 'when the current state is true' do - before do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end - - context 'when previous state was true' do - before do - expect(described_class).to receive(:get_billing_state).and_return(true) - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increment the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end -end diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb new file mode 100644 index 00000000000..0ac946a1232 --- /dev/null +++ b/spec/workers/create_note_diff_file_worker_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe CreateNoteDiffFileWorker do + describe '#perform' do + let(:diff_note) { create(:diff_note_on_merge_request) } + + it 'creates diff file' do + diff_note.note_diff_file.destroy! + + expect_any_instance_of(DiffNote).to receive(:create_diff_file) + .and_call_original + + described_class.new.perform(diff_note.id) + end + end +end |